webpack 构建流程 (待更新。。。)

webpack 构建 流程

webpack 整体是一个插件的架构,所有的功能都是插件的方式集成在构建流程中,通过发布订阅来触发各个插件的执行,webpack 的核心使用Tapable

webpack 关键节点对应的事件

  • entry-option 初始化 option
  • run 开始编译
  • make 从entry 开始递归的分析依赖模块进行build
  • before-resolve -after-resolve 对其中的一个模块进行解析
  • build-module 开始构建这个module 这里将使用文件对应的loader加载
  • normal-module-loader 对用loader加载完成的module(是一段js代码)进行编译,用 acorn 编译,生成ast抽象语法树
  • program 开始对ast进行遍历,当遇到require等一些调用表达式时,触发call require事件的handler执行,收集依赖,并。如:AMDRequireDependenciesBlockParserPlugin等
  • seal 所有依赖build完成,下面将开始对chunk进行优化,比如合并,抽取公共模块,加hash
  • emit 把各个chunk输出到结果文件

webpack 准备阶段
modules 和chunks 的生成阶段
文件生成 阶段

webpack 准备工作

  • 当我们 运行webpack 的时候就会创建Compiler 实例并且加载内部插件,这里跟构建流程相关性比较大的内部插件是 EntryOptionPlugin
  • EntryOptionPlugin 会解析webpack 配置中的entry 属性,然后生成不同的插件应用到Compiler实例上,不管哪个插件内部都会监听Compiler 实例对make任务点

这里的make 任务点将成为后面解析modules和chunks的起点

  • 当Compiler实例加载完成之后,下一步就会直接调用compiler.run方法来启动,值得注意的是此时只有options 属性是解析完成的

  • run 一旦执行就开始了编辑和构建流程 其中比较关键的几个webpack 事件节点

    • compile 开始编译
    • make 从入口点分析模块及其依赖的模块,创建这些模块对象
    • build-module 构建模块
    • after-compile 完成构建
    • seal 封装构建结果
    • emit 把各个chunk输出到结果文件
    • after-emit 完成输出
  • 接下来Compiler 对象开始实例化两个核心工厂的对象 NormalModuleFactory 和 ContextModuleFactory。 这两个对象工厂会在任务点compile 触发时传递过去,所以任务点compile 是间接监听这两个对象任务点的一个入口

module.exports = class EntryOptionPlugin {
    /**
     * @param {Compiler} compiler the compiler instance one is tapping into
     * @returns {void}
     */
    apply(compiler) {
        compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
            if (typeof entry === "string" || Array.isArray(entry)) {
                itemToPlugin(context, entry, "main").apply(compiler);
            } else if (typeof entry === "object") {
                for (const name of Object.keys(entry)) {
                    itemToPlugin(context, entry[name], name).apply(compiler);
                }
            } else if (typeof entry === "function") {
                new DynamicEntryPlugin(context, entry).apply(compiler);
            }
            return true;
        });
    }
};
  • 当compilation 实例创建完成之后,webpack 准备工作已经完成,下一步将开始modules 和chunks 的生成阶段

总结:
1、编译过程启动的入口 compiler.run 开始,触发了一系列的生命周期钩子后,执行 compiler.compile
2、获取 compilation 所需 params,实例化 NormalModuleFactory 类(插件会去注册其钩子) 及 ContextModuleFactory 类,在实例化 NormalModuleFactory 的过程中,会实例化 RuleSet 及注册钩子 factory 和 resolver。
3、实例化 Compilation,传入 params 参数,触发之前在注册 plugin 阶段所注册的 NormalModuleFactory 下的 hooks。
4、触发 make 钩子执行compilation.addEntry,通过编译队列控制 semaphore.acquire 执行moduleFactory.create开始创建 module。

modules 和chunks 的生成阶段

这个阶段的主要内容,是先解析项目依赖的所有的modules ,再根据modules生成chunks;
modules解析主要包含: 创建实例、loader应用以及依赖手机 chunks 生成,主要步骤找到chunk 所需要包含的modules
chunks 生成,主要的步骤就是找到chunk所需要包含的modules

  • 上一个阶段完成,任务点 make 触发,此时内部插件 SingleEntryPlugin 的监听器开始执行,监听器调用Compilation 实例的 addEntry 方法,该方法将第一批module 解析

modules 解析过程

  • 第一个步骤是创建 NormalModule 实例

实例化 NormalModule 得到初始化的 module,然后在 build 过程中先 run loader 处理源码,得到一个编译后的字符串或 buffer。

NormalModuleFactory 的creat 方法 创建 NormalModule 实例的入口 主要解析的 module 需要用到一些属性
比如要用到的 loaders, 资源路径 resource 等等 将解析完的参数给 NormalModule 构建函数直接实例化

  • 在创建完 NormalModule 实例之后会调用 build 方法继续进行内部的构建

    这时候loader 将会开始使用, 应用 loaders 的过程相对简单,直接调用了 loader-runner 这个模块
    NormalModule 最终都是js 模块,所以loader 作用之一就是将不同的资源文件转为 js 模块
    我们需要得到这个 module 所依赖的其他模块,所以就有一个依赖收集的过程。webpack 的依赖收集过程是将 js 源码传给 js parser

    • 调用 parser 将前面 runloaders 的编译结果通过 acorn 转换为 ast;parser 将 js 源码解析后得到对应的AST。 然后 webpack 会遍历 AST,按照一定规则触发任务点

    有了AST的任务点,依赖收集就相对简单了。比如遇到任务点 callrequire,说明在代码中是有调用了 require函数,那么就应该给 module 添加新的依赖。

    • 当 parser 解析完成之后,module 的解析过程就完成了,就会触发任务点 succeed-module。。最终我们会得到所有依赖的module,此时任务点 make 结束
    • Compialtion 实例的 seal 方法会被调用并马上触发任务点 seal。 webpack 会开始生成 chunks

chunk 生成

  • webpack 先将entry 对应的 module 生成一个新的 chunk
  • 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
  • 如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖
  • 重复上面的过程,直至得到所有的 chunks

对module 和chunk 进行优化 操作

  • 这些任务点一般是 webpack.optimize 属性下的插件会使用到

文件生成

根据chunks 生成最终文件 :模板hash 更新,模板渲染chunk,生成文件

webpack 基本框架

  • webpack的基础架构 是基于一种类似时间的方式,webpack 的大部分功能通过注册任务点的方式来实现。
  • 在我们运行 webpack的时候 会注册 一个Compiler 实例,然后 调用WebpackOptionsApply这个模块给 Compiler添加插件

hash 和 chunkhash, contenthash

  • hash: 如果使用hansh 对js css 签名每次都不一样,这样导致无法利用缓存,

  • chunkhash: 根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值,比如我们将一些公共模块,或者第三方依赖包独立开来,接着用chunkhash 生成哈希值,只要不改变公共代码,就不需要重新构建

  • 打包时发现,js和js引入的css的 chunkhash 是相同的,导致无法区分css和js的更新,如下

webpack 把css 当成js 的一部分,所以计算chunkhash 是混合在一起计算的,解决的办法: css是使用 ExtractTextPlugin 插件引入的,这时候可以使用到这个插件提供的 contenthash,

当chunkhash 用在css 中时, 由于css 和js 用了同一个chunkhash,所以当只改变js 时,css 文件也会重新生成, 所以css 中我们使用contenthash

  new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: 'static/css/[name].[contenthash:8].css',
      chunkFilename: 'static/css/[name].[contenthash:8].chunk.css'
    }),
  • 在webpack 中配置 CommonsChunkPlugin (webpack4 已经废弃)

    • 配置webpack的output项时,filename 和 chunkFilename 必要使用chunkhash 不要使用hash,否则
    • 对于抽取的css样式文件,需要使用contenthash, 与file-loader中的hash意义相同。此处不能为chunkhash,否则其与抽取该样式文件的entry chunk的chunkhash保持一致,打不到缓存的目的

webpack.jpg

前面对webpack 部分细节一知半解,又来更新,重新更新 webpack 的打包,webpack 的一些api 也有所更新... 来吧! 再一次宠幸下 webpack 吧!

webpack 运行机制:

下面写贴上 webpack 运行机制 一篇不错的文章,和大致的流程图

https://github.com/jerryOnlyZRJ/webpack-loader/blob/master/docs/webpack-principle.md

image

webpack基本打包机制:
https://juejin.im/post/5df884ad6fb9a0164e7f979d#heading-8
基本的打包过程:

  • 1、利用babel完成代码转换,并生成单个文件的依赖
  • 2、从入口开始递归分析,并生成依赖图谱
  • 3、将各个引用模块打包为一个立即执行函数
  • 4、将最终的bundle文件写入bundle.js中

webpack 打包过程:

  • 1、初始化参数 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数

  • 2、开始编译 用上一步得到的参数初始Compiler对象,加载所有配置的插件,通 过执行对象的run方法开始执行编译

  • 3、确定入口 根据配置中的 Entry 找出所有入口文件

  • 4、编译模块 从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

  • 5、完成模块编译 在经过第4步使用 Loader 翻译完所有模块后, 得到了每个模块被编译后的最终内容及它们之间的依赖关系

  • 6、输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会

  • 7、输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中。

webpack 的事件流Tapable

为啥要说这个 Tapable 呢?
前面说过:webpack 整体是一个插件的架构,所有的功能都是插件的方式集成在构建流程中,通过发布订阅来触发各个插件的执行,webpack 的核心使用Tapable,它允许你创建钩子,为钩子挂载函数(webpack 里面挂载插件)
webpack最核心的模块例如:Compiler、Compilation等,他们都继承于Tapable。

webpack 里面有100多个插件,webpack 本身就像一条生产线,经过一个一个插件去处理,webpack 通过Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 —吴浩麟《深入浅出webpack》

Tapable:
Tapable 通过发布者- 订阅者模式实现, 想具体了解的可以参考Tapable

Tapable 提供了一系列钩子:

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
\
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");

Hook Type:

  • Basic hook: 这个比较简单一点,就是按照tap 注册顺序 一个一个向下就可以了

  • Waterfall :虽然也是按照tap的顺序一个个向下执行,但是如果上一个tap有返回值,那么下一个tap的传入参数就是上一个tap的返回值。

  • Bail : 如果返回了null以外的值,就不继续执行了。

  • Loop :当监听函数被触发的时候,如果该监听函数返回true时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环

// 钩子语法
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

举个例子:

class MyDaily {
    constructor() {
        this.hooks = {
            beforeWork: new SyncHook(["getUp"]),
            atWork: new SyncWaterfallHook(["workTask"]),
            afterWork: new SyncBailHook(["activity"])
        };
    }
    tapTap(){
        //此处是行为
    }
    run(){
        this.hooks.beforeWork.call()
        this.hooks.atWork.call()
        this.hooks.afterWork.call()
    }
}
tapTap(){
    this.hooks.beforeWork.tap("putOnCloth",()=>{
        console.log("穿衣服!")
    })
    this.hooks.beforeWork.tap("getOut",()=>{
        console.log("出门!")
    })
    this.hooks.atWork.tap("makePPT",()=>{
        console.log("做PPT!")
        return "你的ppt"
    })
    this.hooks.atWork.tap("meeting",(work)=>{
        console.log("带着你的"+work+"开会!")
    })
    this.hooks.afterWork.tap("haveADate",()=>{
        console.log("约会咯!")
        return "约会真开心~"
    })
    this.hooks.afterWork.tap("goHome",()=>{
        console.log("溜了溜了!")
    })
}
//是熔断型的钩子,这个钩子的存在意义就是,可以中断一系列的事情,比如有地方出错了,或者不需要进行下一步的操作我们就可以及时结束

上面都是同步类型,如果我们做的事情是异步的呢?

this.hooks.beforeWork.tapAsync("putOnCloth",(params,callback)=>{
    console.log("穿衣服!")
    callback();//此处无callback,则getOut这个挂载的函数便不会运行
})
this.hooks.beforeWork.tapAsync("getOut",(params,callback)=>{
    console.log("出门!")
    callback()//此处无callback,则beforeWork这个钩子的回调函数不会执行
})
this.hooks.beforeWork.callAsync("working",err=>{
    console.log(err+" end!")//如果最后一个tap的函数没有callback则不会执行
})

可以将callback当作next函数,也就是下一个tap的函数的意思。 如果函数出错,后面的函数不会运行,如果里面加入错误原因,callback("errorReason"),那么就直接回调用当前钩子的callAsync绑定的函数。

this.hooks.beforeWork.tapAsync("putOnCloth",(params,callback)=>{
    console.log("穿衣服!")
    callback("error");此处加入了错误原因,那么直接callAsync,抛弃了getOut
})
this.hooks.beforeWork.tapAsync("getOut",(params,callback)=>{//直接skip了
    console.log("出门!")
})
this.hooks.beforeWork.callAsync("working",err=>{
    console.log(err+" end!")//error end!直接打出错误原因。
})

Async和sync的区别在于Async通过callback来和后续的函数沟通,
sync则是通过return一个值来做交流。所以,Async自带sync中bail类型的钩子。

webpack 的整个运行机制 都是建立在tapable 之上的,webpack 最核心的模块 compiler、compilation 都是集成于 Tapable

webpack成功的原因之一就是灵活的插件机制,webpack一共提供了180多个勾子,你可以在任何位置,挂载任何的插件,来做一系列的操作。并且本身webpack的运行机制就是:声明一系列勾子,挂载一系列内部插件、调用插件

上面介绍了webpack 里面的事件流机制,下面就开始进入webpack 运行:

1、前面介绍过 EntryOptionPlugin阶段, webpack开始读取配置entry ,递归遍历所有的入口。
|
|
2、Webpack进入其中一个入口文件,开始compilation过程;(使用用户配置好的loader对文件内容进行编译(buildModule),我们可以从传入事件回调的compilation上拿到module的resource(资源路径)、loaders(经过的loaders)等信息)
|
|
3、 将编译好的文件内容使用acorn 解析成AST 静态语法树;分析文件的依赖关系逐个拉取依赖模块并重复上述过程,最后将所有模块中的require语法替换成。
|
|
4、 emit阶段,所有文件的编译及转化都已经完成,包含了最终输出的资源,

webpack4 splitChunk:

而在webpack4中CommonsChunkPlugin被移除,取而代之的是 optimization.splitChunks 和 optimization.runtimeChunk 配置项,

webpack 将根据以下条件自动拆分代码块:

  • 会被共享的代码块或者 node_mudules 文件夹中的代码块
  • 体积大于30KB的代码块(在gz压缩前)(可修改)
  • 按需加载代码块时的并行请求数量不超过5个(可修改)
  • 加载初始页面时的并行请求数量不超过3个(可修改)

splitChunk

splitChunks: {
    chunks: "async”,//默认作用于异步chunk,值为all/initial/async/function(chunk),值为function时第一个参数为遍历所有入口chunk时的chunk模块,chunk._modules为gaichunk所有依赖的模块,通过chunk的名字和所有依赖模块的resource可以自由配置,会抽取所有满足条件chunk的公有模块,以及模块的所有依赖模块,包括css
    minSize: 30000,  //默认值是30kb
    minChunks: 1,  //被多少模块共享
    maxAsyncRequests: 5,  //所有异步请求不得超过5个
    maxInitialRequests: 3,  //初始话并行请求不得超过3个
    name: true,  //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
    cacheGroups: { //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
       common: {
         name: 'common',  //抽取的chunk的名字
         chunks(chunk) { //同外层的参数配置,覆盖外层的chunks,以chunk为维度进行抽取
         },
         test(module, chunks) {  //可以为字符串,正则表达式,函数,以module为维度进行抽取,只要是满足条件的module都会被抽取到该common的chunk中,为函数时第一个参数是遍历到的每一个模块,第二个参数是每一个引用到该模块的chunks数组。自己尝试过程中发现不能提取出css,待进一步验证。
         },
        priority: 10,  //优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中
       minChunks: 2,  //最少被几个chunk引用
       reuseExistingChunk: true,//  如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码
       enforce: true  // 如果cacheGroup中没有设置minSize,则据此判断是否使用上层的minSize,true:则使用0,false:使用上层minSize
       }
    }
}

  • 假如如果一个模块被多个chunks使用,(分割出它之后)就能很容易的在这些chunks之间共享
// a.js

import _ from 'lodash';
import users from './users';

const adam = _.find(users, { firstName: 'Adam' });

// b.js

import _ from 'lodash';
import users from './users';

const lucy = _.find(users, { firstName: 'Lucy' });

// webpack.config.js

module.exports = {
  entry: {
    a: "./src/a.js",
    b: "./src/b.js"
  },
  output: {
    filename: "[name].[chunkhash].bundle.js",
    path: __dirname + "/dist"
  },
  optimization: {
    splitChunks: {
      chunks: "all"
    }
  },
};

// a.[chunkhash].bundle.js和b.[chunkhash].bundle.js
// vendors~a~b.[chunkhash].bundle.js的文件,其包含了Lodash库

SplitChunksPlugin插件默认只分割超过30kb的文件, 如果是 user.js 不超过30kb 不会被单独打包出来的。修改配置 也可单独打包出来


module.exports = {
  entry: {
    a: "./src/a.js",
    b: "./src/b.js"
  },
  output: {
    filename: "[name].[chunkhash].bundle.js",
    path: __dirname + "/dist"
  },
  optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 0
    }
  }
};
//  多加一个 一个名为a~b.[chunkhash].bundle.js的文件
// 因为(如分割)并不能带来性能确实的提升,反而使得浏览器多了一次对utilities.js的请求,
//而这个utilities.js又是如此之小(不划算)

举个例子:

//info.js
const a = 1
export a
// index.js
import info from './info.js'
console.log(info)

//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))

打包之后:


WechatIMG558.png

要注意的是,webpack4中只有optimization.namedModules为true,此时moduleId才会为模块路径,否则是数字id。为了方便开发者调试,在development模式下optimization.namedModules参数默认为true。

我们可能需要了解到:

  • @babel/parser:负责将代码解析为抽象语法树
  • @babel/traverse:遍历抽象语法树的工具,我们可以在语法树中解析特定的节点,然后做一些操作,如ImportDeclaration获取通过import引入的模块,FunctionDeclaration获取函数
  • @babel/core:代码转换,如ES6的代码转为ES5的模式
// test.js
export default function cc() {
    console.log("cc");
}
// index.js
import cc from './test.js';
console.log(cc)

打包后的 转化成函数

// test.js
function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    /* harmony export (binding) */
    __webpack_require__.d(__webpack_exports__, "a",
    function() {
        return cc;
    });
    // 模块中的代码
    function cc() {
        console.log("cc");
    }

}

模块运行时候被解析成对象:

// 'src/test.js'
{
  i: moduleId, // 模块id
  l: false, //  是否已加载
  exports: {
      __esModule: bool, // 是否是es模块
      cc: fn // **
  }
}
// 缓存起来避免重复解析,其中key是文件名

{
    'src/test.js': { // key是文件名
        i: moduleId, // 模块id
        l: false, //  是否已加载
        exports: {
            __esModule: bool,
            cc: fn // **
        }
    }
}

我们简单看下打包之后的代码:

image.png

有个Module对象,key是模块名,value是代码块。输出的也是立即执行函数,从入口开始执行...

webpack_require实现

image.png

这里的moduleId就是模块路径,如./src/commonjs/index.js。压缩之后变成数字

要注意的是,webpack4中只有optimization.namedModules为true,此时moduleId才会为模块路径,否则是数字id。为了方便开发者调试,在development模式下optimization.namedModules参数默认为true。

splitChunks配置:

tree shaking

tree shaking出手的地方了,因为它能帮助我们干掉那些死代码,大大减少打包的尺寸。

  • 必须使用ES6模块,不能使用其它类型的模块如CommonJS之流。

  • 需要使用UglifyJsPlugin插件。如果在mode:"production"模式,这个插件已经默认添加了,如果在其它模式下,可以手工添加它。

  • 打开optimization.usedExports。在mode: "production"模式下,它也是默认打开了的。它告诉webpack每个模块明确使用exports。

  • 记得配置optimization,把usedExports和sideEffects设为true

参考文章:
https://juejin.im/post/5de099886fb9a071562facad
插件与Tapable
webpack 优化
Tapable 详解
http://taobaofed.org/blog/2016/09/09/webpack-flow/
https://lihuanghe.github.io/2016/05/30/webpack-source-analyse.html
winty Webpack4打包机制原理简析

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