原来rollup这么简单之 rollup.rollup篇

大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。
内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。
分享不易,希望能够得到大家的支持和关注。

计划

rollup系列打算一章一章的放出,内容更精简更专一更易于理解

目前打算分为以下几章:

TL;DR

在进入枯燥的代码解析之前,先大白话说下整个过程,rollup.rollup()主要分为以下几步:

  1. 配置收集、标准化
  2. 文件分析
  3. 源码编译,生成ast
  4. 模块生成
  5. 依赖解析
  6. 过滤净化
  7. 产出chunks

按照这个思路来看其实很简单,但是具体的细节却是百般复杂的。
不过我们也不必纠结于具体的某些实现,毕竟条条大路通罗马,我们可以吸纳并改进或学习一些没见过的代码技巧或优化方法,在我看来,这才是良好的阅读源码的方式。:)

注意点

所有的注释都在这里,可自行阅读

!!!版本 => 笔者阅读的rollup版本为: 1.32.0

!!!提示 => 标有TODO为具体实现细节,会视情况分析。

!!!注意 => 每一个子标题都是父标题(函数)内部实现

!!!强调 => rollup中模块(文件)的id就是文件地址,所以类似resolveID这种就是解析文件地址的意思,我们可以返回我们想返回的文件id(也就是地址,相对路径、决定路径)来让rollup加载

rollup是一个核心,只做最基础的事情,比如提供默认模块(文件)加载机制, 比如打包成不同风格的内容,我们的插件中提供了加载文件路径,解析文件内容(处理ts,sass等)等操作,是一种插拔式的设计,和webpack类似
插拔式是一种非常灵活且可长期迭代更新的设计,这也是一个中大型框架的核心,人多力量大嘛~

主要通用模块以及含义

  1. Graph: 全局唯一的图,包含入口以及各种依赖的相互关系,操作方法,缓存等。是rollup的核心
  2. PathTracker: 无副作用模块依赖路径追踪
  3. PluginDriver: 插件驱动器,调用插件和提供插件环境上下文等
  4. FileEmitter: 资源操作器
  5. GlobalScope: 全局作用局,相对的还有局部的
  6. ModuleLoader: 模块加载器
  7. NodeBase: ast各语法(ArrayExpression、AwaitExpression等)的构造基类

主流程解析

  • 1.调用getInputOptions标准化input配置参数

    const inputOptions = getInputOptions(rawInputOptions);
    
    • 1.1. 调用mergeOptions,设置默认的input和output配置,并返回input配置 和 使用非法配置属性的错误信息
      let { inputOptions, optionError } = mergeOptions({
        config: rawInputOptions
      });
      
    • 1.2. 调用options钩子函数,以在input配合完全标准化之前进行自定义修改
      inputOptions = inputOptions.plugins!.reduce(applyOptionHook, inputOptions);
      
    • 1.3. 标准化插件操作:为返回对象中没有name属性的插件设置默认的插件名 => at position 当前插件在所有插件中索引值
      inputOptions.plugins = normalizePlugins(inputOptions.plugins!, ANONYMOUS_PLUGIN_PREFIX);
      
    • 1.4. 对不兼容内嵌动态引入模块或保留模块两种情况的配置,进行警告报错
      // 将动态导入的依赖(import | require.ensure() | other)内嵌到一个chunk而不创建独立的包,相关的代码逻辑如下
      if (inputOptions.inlineDynamicImports) {
        // preserveModules: 尽可能的保留模块,而不是混合起来,创建更少的chunks,默认为false,不开启
        if (inputOptions.preserveModules) // 如果开启了,就与内嵌冲突了
          return error({
            code: 'INVALID_OPTION',
            message: `"preserveModules" does not support the "inlineDynamicImports" option.`
          });
        // 其他判断,具体参考代码仓库:index.ts
      } else if (inputOptions.preserveModules) {
        // 又对 以原始文件命名,不综合打包 的功能进行排异处理
        if (inputOptions.manualChunks)
          return error({
            code: 'INVALID_OPTION',
            message: '"preserveModules" does not support the "manualChunks" option.'
          });
        // 其他判断,具体参考代码仓库:index.ts
      }
      
    • 1.5. 返回处理后的input配置
      return inputOptions;
      
  • 2.是否开启性能检测,检测inputOptions.perf属性,如果未设置没那么检测函数为空

    initialiseTimers(inputOptions);
    
  • 3.创建图,参数为input配置和watch,watch当前不考虑

    const graph = new Graph(inputOptions, curWatcher);
    
    • 3.1. 初始化警告函数,对已经提示过得警告进行缓存

      this.onwarn = (options.onwarn as WarningHandler) || makeOnwarn();
      
    • 3.2. 给当前图挂载路径追踪系统,无构造函数,只有属性和更改属性的方法

      this.deoptimizationTracker = new PathTracker();
      
    • 3.3. 初始化当前图的唯一模块缓存容器,可以将上个打包结果的cache属性赋给下一次打包,提升打包速度 =>

        this.cachedModules = new Map();
      
    • 3.4. 读取传递的上次build结果中的模块和插件。插件缓存参考 =>,下文中解释。

      if (options.cache) {
        if (options.cache.modules)
          for (const module of options.cache.modules) this.cachedModules.set(module.id, module);
      }
      
      if (options.cache !== false) {
        this.pluginCache = (options.cache && options.cache.plugins) || Object.create(null);
      
        for (const name in this.pluginCache) {
          const cache = this.pluginCache[name];
          for (const key of Object.keys(cache)) cache[key][0]++;
        }
      }
      
    • 3.5. treeshake信息挂载。

      if (options.treeshake !== false) {
        this.treeshakingOptions =
          options.treeshake && options.treeshake !== true
            ? {
                annotations: options.treeshake.annotations !== false,
                moduleSideEffects: options.treeshake.moduleSideEffects,
                propertyReadSideEffects: options.treeshake.propertyReadSideEffects !== false,
                pureExternalModules: options.treeshake.pureExternalModules,
                tryCatchDeoptimization: options.treeshake.tryCatchDeoptimization !== false,
                unknownGlobalSideEffects: options.treeshake.unknownGlobalSideEffects !== false
              }
            : {
                annotations: true,
                moduleSideEffects: true,
                propertyReadSideEffects: true,
                tryCatchDeoptimization: true,
                unknownGlobalSideEffects: true
              };
        if (typeof this.treeshakingOptions.pureExternalModules !== 'undefined') {
          this.warnDeprecation(
            `The "treeshake.pureExternalModules" option is deprecated. The "treeshake.moduleSideEffects" option should be used instead. "treeshake.pureExternalModules: true" is equivalent to "treeshake.moduleSideEffects: 'no-external'"`,
            false
          );
        }
      }
      
    • 3.6. 初始化代码解析器,具体参数和插件参考Graph.ts

      this.contextParse = (code: string, options: acorn.Options = {}) =>
        this.acornParser.parse(code, {
          ...defaultAcornOptions,
          ...options,
          ...this.acornOptions
        }) as any;
      
    • 3.7. 插件驱动器

      this.pluginDriver = new PluginDriver(
        this,
        options.plugins!,
        this.pluginCache,
        // 处理软连文件的时候,是否以为软连所在地址作为上下文,false为是,true为不是。
        options.preserveSymlinks === true,
        watcher
      );
      
      • 3.7.1. 弃用api警告,参数挂载

      • 3.7.2. 实例化FileEmitter并且将实例所携带方法设置到插件驱动器上

        // basePluginDriver为PluginDriver的第六个参数,代表graph的'根'插件驱动器
        this.fileEmitter = new FileEmitter(graph, basePluginDriver && basePluginDriver.fileEmitter);
        this.emitFile = this.fileEmitter.emitFile;
        this.getFileName = this.fileEmitter.getFileName;
        this.finaliseAssets = this.fileEmitter.assertAssetsFinalized;
        this.setOutputBundle = this.fileEmitter.setOutputBundle;
        
      • 3.7.3. 插件拼接

        this.plugins = userPlugins.concat(
          basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)] 
        );
        
      • 3.7.4. 缓存插件们的上下文环境,之后执行插件的的时候会通过index获取并注入到插件内

        // 利用map给每个插件注入plugin特有的context,并缓存
        this.pluginContexts = this.plugins.map(
          getPluginContexts(pluginCache, graph, this.fileEmitter, watcher)
        );
        
      • 3.7.5. input和output设置的插件冲突的时候,报错

        if (basePluginDriver) {
          for (const plugin of userPlugins) {
            for (const hook of basePluginDriver.previousHooks) {
              if (hook in plugin) {
                graph.warn(errInputHookInOutputPlugin(plugin.name, hook));
              }
            }
          }
        }
        
    • 3.8. 监听模式的设定

          if (watcher) {
              const handleChange = (id: string) => this.pluginDriver.hookSeqSync('watchChange', [id]);
              watcher.on('change', handleChange);
              watcher.once('restart', () => {
                  watcher.removeListener('change', handleChange);
              });
          }
      
    • 3.9. 全局上下文

      this.scope = new GlobalScope();
      
    • 3.10. 设置模块的全局上下文,默认为false

      this.context = String(options.context);
      
          // 用户是否自定义了上下文环境
          const optionsModuleContext = options.moduleContext;
          if (typeof optionsModuleContext === 'function') {
              this.getModuleContext = id => optionsModuleContext(id) || this.context;
          } else if (typeof optionsModuleContext === 'object') {
              const moduleContext = new Map();
              for (const key in optionsModuleContext) {
                  moduleContext.set(resolve(key), optionsModuleContext[key]);
              }
              this.getModuleContext = id => moduleContext.get(id) || this.context;
          } else {
              this.getModuleContext = () => this.context;
          }
      
    • 3.11. 初始化moduleLoader,用于模块(文件)的解析和加载

      // 模块(文件)解析加载,内部调用的resolveID和load等钩子,让使用者拥有更多的操作能力
      this.moduleLoader = new ModuleLoader(
              this,
              this.moduleById,
              this.pluginDriver,
              options.external!,
              (typeof options.manualChunks === 'function' && options.manualChunks) as GetManualChunk | null,
              (this.treeshakingOptions ? this.treeshakingOptions.moduleSideEffects : null)!,
              (this.treeshakingOptions ? this.treeshakingOptions.pureExternalModules : false)!
          );
      
  • 4.执行buildStart钩子函数,打包获取chunks,以供后续生成和写入使用

    try {
          // buildStart钩子函数触发
          await graph.pluginDriver.hookParallel('buildStart', [inputOptions]);
          // 这一步通过id,深度分析拓扑关系,去除无用块,进而生成我们的chunks
      
      // build的逻辑详见下文
          chunks = await graph.build( // 这个chunks是闭包,所以generate和write可以用到
              inputOptions.input as string | string[] | Record<string, string>,
              inputOptions.manualChunks,
              inputOptions.inlineDynamicImports!
          );
      } catch (err) {
          const watchFiles = Object.keys(graph.watchFiles);
          if (watchFiles.length > 0) {
              err.watchFiles = watchFiles;
          }
          await graph.pluginDriver.hookParallel('buildEnd', [err]);
          throw err;
      }
    
  • 5.返回一个对象,包括缓存,监听文件和generate、write两个方法

    return {
      cache,
      watchFiles,
      generate,
      write
    }
    
graph.build逻辑解析

build方法通过id,深度分析拓扑关系,去除无用块,进而生成我们的chunks
接受三个参数:入口、提取公共块规则(manualChunks)、是否内嵌动态导入模块

  • build是很单一的方法,就是产出我们的chunks。他返回一个promise对象供之后的使用。
      return Promise.all([
        入口模块, // 代码为: this.moduleLoader.addEntryModules(normalizeEntryModules(entryModules), true)
        用户定义公共模块 // 这块没有返回值,只是将公共模块缓存到模块加载器上,处理结果由入口模块代理返回。巧妙的处理方式,一举两得
      ]).then((入口模块的返回) => {
        // 模块的依赖关系处理
        return chunks;
      });
    
  • 入口模块: this.moduleLoader.addEntryModules(normalizeEntryModules(entryModules), true)
    • normalizeEntryModules对入口进行标准化处理,返回统一的格式:
        UnresolvedModule {
            fileName: string | null;
            id: string;
            name: string | null;
        }
      
    • addEntryModules对模块进行加载、去重,再排序操作,最后返回模块,公共chunks。其中,在加载过程中会将处理过的模块缓存到ModuleLoaders的modulesById(Map对象)上。部分代码如下:
        // 模块加载部分
        private fetchModule(
          id: string,
          importer: string,
          moduleSideEffects: boolean,
          syntheticNamedExports: boolean,
          isEntry: boolean
        ): Promise<Module> {
          // 主流程如下:
          
          // 获取缓存,提升效率:
          const existingModule = this.modulesById.get(id);
          if (existingModule instanceof Module) {
            existingModule.isEntryPoint = existingModule.isEntryPoint || isEntry;
            return Promise.resolve(existingModule);
          }
          
          // 新建模块:
          const module: Module = new Module(
            this.graph,
            id,
            moduleSideEffects,
            syntheticNamedExports,
            isEntry
          );
          
          // 缓存,以备优化
          this.modulesById.set(id, module);
          
          // 为每一个入库模块设置已监听
          this.graph.watchFiles[id] = true;
          
          // 调用用户定义的manualChunk方法,获取公共chunks别名,比如:
          // 比如 manualChunkAlias(id){
          //  if (xxx) {
          //      return 'vendor';
          //  }
          // }
          const manualChunkAlias = this.getManualChunk(id);
          
          // 缓存到 manualChunkModules
          if (typeof manualChunkAlias === 'string') {
            this.addModuleToManualChunk(manualChunkAlias, module);
          }
          
          // 调用load钩子函数并返回处理结果,其中第二个数组参数为传到钩子函数的的参数
          return Promise.resolve(this.pluginDriver.hookFirst('load', [id]))
            .cache()
            .then(source => {
              // 统一格式: sourceDescription
              return {
                code: souce,
                // ...
              }
            })
            .then(sourceDescription => {
              // 返回钩子函数transform处理后的代码,比如jsx解析结果,ts解析结果
              // 参考: https://github.com/rollup/plugins/blob/e7a9e4a516d398cbbd1fa2b605610517d9161525/packages/wasm/src/index.js
              return transform(this.graph, sourceDescription, module);
            })
            .then(source => {
              // 代码编译结果挂在到当前解析的入口模块上
              module.setSource(source);
              // 模块id与模块绑定
              this.modulesById.set(id, module);
              // 处理模块的依赖们,将导出的模块也挂载到module上
              // !!! 注意: fetchAllDependencies中创建的模块是通过ExternalModule类创建的,有别的入口模块的
              return this.fetchAllDependencies(module).then(() => {
                for (const name in module.exports) {
                  if (name !== 'default') {
                    module.exportsAll[name] = module.id;
                  }
                }
                for (const source of module.exportAllSources) {
                  const id = module.resolvedIds[source].id;
                  const exportAllModule = this.modulesById.get(id);
                  if (exportAllModule instanceof ExternalModule) continue;
      
                  for (const name in exportAllModule!.exportsAll) {
                    if (name in module.exportsAll) {
                      this.graph.warn(errNamespaceConflict(name, module, exportAllModule!));
                    } else {
                      module.exportsAll[name] = exportAllModule!.exportsAll[name];
                    }
                  }
                }
              // 返回这些处理后的module对象,从id(文件路径) 转换到 一个近乎具有文件完整信息的对象。
              return module;
            })
          
        }
      
        // 去重
        let moduleIndex = firstEntryModuleIndex;
              for (const entryModule of entryModules) {
                  // 是否为用户定义,默认是
                  entryModule.isUserDefinedEntryPoint = entryModule.isUserDefinedEntryPoint || isUserDefined;
                  const existingIndexModule = this.indexedEntryModules.find(
                      indexedModule => indexedModule.module.id === entryModule.id
                  );
                  // 根据moduleIndex进行入口去重
                  if (!existingIndexModule) {
                      this.indexedEntryModules.push({ module: entryModule, index: moduleIndex });
                  } else {
                      existingIndexModule.index = Math.min(existingIndexModule.index, moduleIndex);
                  }
                  moduleIndex++;
              }
        // 排序
        this.indexedEntryModules.sort(({ index: indexA }, { index: indexB }) =>
                  indexA > indexB ? 1 : -1
              );
      
  • 模块的依赖关系处理 部分
    • 已经加载处理过的模块会缓存到moduleById上,所以直接遍历之,再根据所属模块类进行分类

        // moduleById是 id => module 的存储, 是所有合法的入口模块
              for (const module of this.moduleById.values()) {
                  if (module instanceof Module) {
                      this.modules.push(module);
                  } else {
                      this.externalModules.push(module);
                  }
              }
      
    • 获取所有入口,找到正确的、移除无用的依赖,并过滤出真正作为入口的模块

        // this.link(entryModules)方法的内部
        
        // 找到所有的依赖
        for (const module of this.modules) {
          module.linkDependencies();
        }
        
        // 返回所有的入口启动模块(也就是非外部模块),和那些依赖了一圈结果成死循环的模块相对路径
        const { orderedModules, cyclePaths } = analyseModuleExecution(entryModules);
        
        // 对那些死循环路径进行警告
        for (const cyclePath of cyclePaths) {
          this.warn({
            code: 'CIRCULAR_DEPENDENCY',
            cycle: cyclePath,
            importer: cyclePath[0],
            message: `Circular dependency: ${cyclePath.join(' -> ')}`
          });
        }
        
        // 过滤出真正的入口启动模块,赋值给modules
        this.modules = orderedModules;
        
        // ast语法的进一步解析
        // TODO: 视情况详细补充
        for (const module of this.modules) {
          module.bindReferences();
        }
        
      
    • 剩余部分

        // 引入所有的导出,设定相关关系
        // TODO: 视情况详细补充
          for (const module of entryModules) {
                  module.includeAllExports();
              }
        
        // 根据用户的treeshaking配置,给引入的环境设置上下文环境
              this.includeMarked(this.modules);
        
              // 检查所有没使用的模块,进行提示警告
              for (const externalModule of this.externalModules) externalModule.warnUnusedImports();
        
        // 给每个入口模块添加hash,以备后续整合到一个chunk里
        if (!this.preserveModules && !inlineDynamicImports) {
                  assignChunkColouringHashes(entryModules, manualChunkModulesByAlias);
              }
        
        let chunks: Chunk[] = [];
        
        // 为每个模块都创建chunk
              if (this.preserveModules) {
                  // 遍历入口模块
                  for (const module of this.modules) {
                      // 新建chunk实例对象
                      const chunk = new Chunk(this, [module]);
                      // 是入口模块,并且非空
                      if (module.isEntryPoint || !chunk.isEmpty) {
                          chunk.entryModules = [module];
                      }
                      chunks.push(chunk);
                  }
              } else {
                  // 创建尽可能少的chunk
                  const chunkModules: { [entryHashSum: string]: Module[] } = {};
                  for (const module of this.modules) {
                      // 将之前设置的hash值转换为string
                      const entryPointsHashStr = Uint8ArrayToHexString(module.entryPointsHash);
                      const curChunk = chunkModules[entryPointsHashStr];
                      // 有的话,添加module,没有的话创建并添加,相同的hash值会添加到一起
                      if (curChunk) {
                          curChunk.push(module);
                      } else {
                          chunkModules[entryPointsHashStr] = [module];
                      }
                  }
      
                  // 将同一hash值的chunks们排序后,添加到chunks中
                  for (const entryHashSum in chunkModules) {
                      const chunkModulesOrdered = chunkModules[entryHashSum];
                      // 根据之前的设定的index排序,这个应该代表引入的顺序,或者执行的先后顺序
                      sortByExecutionOrder(chunkModulesOrdered);
                      // 用排序后的chunkModulesOrdered新建chunk
                      const chunk = new Chunk(this, chunkModulesOrdered);
                      chunks.push(chunk);
                  }
              }
        
        // 将依赖挂载到每个chunk上
              for (const chunk of chunks) {
                  chunk.link();
              }
      

以上就是rollup.rollup的主流程分析,具体细节参考代码库注释

部分功能的具体解析

  • 插件缓存能力解析,为开发者们提供了插件上的缓存能力,利用cacheKey可以共享相同插件的不同实例间的数据
function createPluginCache(cache: SerializablePluginCache): PluginCache {
    // 利用闭包将cache缓存
    return {
        has(id: string) {
            const item = cache[id];
            if (!item) return false;
            item[0] = 0; // 如果访问了,那么重置访问过期次数,猜测:就是说明用户有意向主动去使用
            return true;
        },
        get(id: string) {
            const item = cache[id];
            if (!item) return undefined;
            item[0] = 0; // 如果访问了,那么重置访问过期次数
            return item[1];
        },
        set(id: string, value: any) {
            cache[id] = [0, value];
        },
        delete(id: string) {
            return delete cache[id];
        }
    };
}

可以看到rollup利用对象加数组的结构来为插件提供缓存能力,即:

{
  test: [0, '内容']
}

数组的第一项是当前访问的计数器,和缓存的过期次数挂钩,再加上js的闭包能力简单实用的提供了插件上的缓存能力

总结

到目前为止,再一次加深了职能单一和依赖注入重要性,比如模块加载器,插件驱动器,还有Graph。还有rollup的(数据)模块化,webpack也类似,vue也类似,都是将具象的内容转换为抽象的数据,再不断挂载相关的依赖的其他抽象数据,当然这其中需要符合某些规范,比如estree规范

鄙人一直对构建很感兴趣,我的github有接近一半都是和构建有关的,所以这次从rollup入口,开始揭开构建世界的那一层层雾霾,还我们一个清晰地世界。:)

rollup系列不会参考别人的分享(目前也没找到有人分析rollup。。),完全自食其力一行一行的阅读,所以难免会有些地方不是很正确。
没办法,阅读别人的代码,有些地方就像猜女人的心思,太tm难了,所以有不对的地方希望大佬们多多指点,互相学习。

image

还是那句话,创作不易,希望得到大家的支持,与君共勉,咱们下期见!

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容