[前端進階課] 構建自己的 webpack 知識體系

webpack

webpack 最出色的功能之一就是,除了 JavaScript,還可以通過 loader 引入任何其他類型的文件

Webpack 核心概念:

  • Entry(入口):Webpack 執行構建的第一步將從 Entry 開始,可抽象成輸入。
  • Output(出口):指示 webpack 如何去輸出、以及在哪里輸出
  • Module(模塊):在 Webpack 里一切皆模塊,一個模塊對應著一個文件。Webpack 會從配置的 Entry 開始遞歸找出所有依賴的模塊。
  • Chunk(代碼塊):一個 Chunk 由多個模塊組合而成,用于代碼合并與分割。
  • Loader(模塊轉換器):用于把模塊原內容按照需求轉換成新內容。
  • Plugin(擴展插件):在 Webpack 構建流程中的特定時機會廣播出對應的事件,插件可以監聽這些事件,并改變輸出結果

配置項

  1. 入口 Entry
entry: {
  a: "./app/entry-a",
  b: ["./app/entry-b1", "./app/entry-b2"]
},

多入口可以通過 HtmlWebpackPlugin 分開注入

plugins: [
  new HtmlWebpackPlugin({
    chunks: ['a'],
    filename: 'test.html',
    template: 'src/assets/test.html'
  })
]
  1. 出口 Output

修改路徑相關

  • publicPath:并不會對生成文件的目錄造成影響,主要是對你的頁面里面引入的資源的路徑做對應的補全
  • filename:能修改文件名,也能更改文件目錄

導出庫相關

  • library: 導出庫的名稱
  • libraryTarget: 通用模板定義方式
  1. 模塊 Module

webpack 一切皆模塊,配置項 Module,定義模塊的各種操作,

Module 主要配置:

  • loader: 各種模塊轉換器
  • extensions:使用的擴展名
  • alias:別名、例如:vue-cli 常用的 @ 出自此處
  1. 其他
  • plugins: 插件列表
  • devServer:開發環境相關配置,譬如 proxy
  • externals:打包排除模塊
  • target:包應該運行的環境,默認 web

Webpack 執行流程

webpack從啟動到結束會依次執行以下流程:

  1. 初始化:解析webpack配置參數,生產 Compiler 實例
  2. 注冊插件:調用插件的apply方法,給插件傳入compiler實例的引用,插件通過compiler調用Webpack提供的API,讓插件可以監聽后續的所有事件節點。
  3. 入口:讀取入口文件
  4. 解析文件:使用loader將文件解析成抽象語法樹 AST
  5. 生成依賴圖譜:找出每個文件的依賴項(遍歷)
  6. 輸出:根據轉換好的代碼,生成 chunk
  7. 生成最后打包的文件

ps:由于 webpack 是根據依賴圖動態加載所有的依賴項,所以,每個模塊都可以明確表述自身的依賴,可以避免打包未使用的模塊。

Babel

Babel 是一個工具鏈,主要用于將 ECMAScript 2015+ 版本的代碼轉換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環境中:

Babel 內部所使用的語法解析器是 Babylon

主要功能

  • 語法轉換
  • 通過 Polyfill 方式在目標環境中添加缺失的特性 (通過 @babel/polyfill 模塊)
  • 源碼轉換 (codemods)

主要模塊

  • @babel/parser:負責將代碼解析為抽象語法樹
  • @babel/traverse:遍歷抽象語法樹的工具,我們可以在語法樹中解析特定的節點,然后做一些操作
  • @babel/core:代碼轉換,如ES6的代碼轉為ES5的模式

Webpack 打包結果

在使用 webpack 構建的典型應用程序或站點中,有三種主要的代碼類型:

  1. 源碼:你或你的團隊編寫的源碼。
  2. 依賴:你的源碼會依賴的任何第三方的 library 或 "vendor" 代碼。
  3. 管理文件:webpackruntime 使用 manifest 管理所有模塊的交互。

runtime:在模塊交互時,連接模塊所需的加載和解析邏輯。包括瀏覽器中的已加載模塊的連接,以及懶加載模塊的執行邏輯。

manifest:當編譯器(compiler)開始執行、解析和映射應用程序時,它會保留所有模塊的詳細要點。這個數據集合稱為 "Manifest",
當完成打包并發送到瀏覽器時,會在運行時通過 Manifest 來解析和加載模塊。無論你選擇哪種模塊語法,那些 import 或 require 語句現在都已經轉換為 webpack_require 方法,此方法指向模塊標識符(module identifier)。通過使用 manifest 中的數據,runtime 將能夠查詢模塊標識符,檢索出背后對應的模塊。

其中:

  • importrequire 語句會轉換為 __webpack_require__
  • 異步導入會轉換為 require.ensure(在Webpack 4 中會使用 Promise 封裝)

比較

  • gulp 是任務執行器(task runner):就是用來自動化處理常見的開發任務,例如項目的檢查(lint)、構建(build)、測試(test)
  • webpack 是打包器(bundler):幫助你取得準備用于部署的 JavaScript 和樣式表,將它們轉換為適合瀏覽器的可用格式。例如,JavaScript 可以壓縮、拆分 chunk 和懶加載,

實現一個 loader

loader 就是一個js文件,它導出了一個返回了一個 buffer 或者 string 的函數;

譬如:

// log-loader.js
module.exports = function (source) {
  console.log('test...', source)
  return source
}

在 use 時,如果 log-loader 并沒有在 node_modules 中,那么可以使用路徑導入。

實現一個 plugin

plugin: 是一個含有 apply 方法的

譬如:

class DemoWebpackPlugin {
    constructor () {
        console.log('初始化 插件')
    }
    apply (compiler) {
    }
}

module.exports = DemoWebpackPlugin

apply 方法中接收一個 compiler 參數,也就是 webpack實例。由于該參數的存在 plugin 可以很好的運用 webpack 的生命周期鉤子,在不同的時間節點做一些操作。

Webpack 優化概況

Webpack 加快打包速度的方法

  1. 使用 includeexclude 加快文件查找速度
  2. 使用 HappyPack 開啟多進程 Loader 轉換
  3. 使用 ParallelUglifyPlugin 開啟多進程 JS 壓縮
  4. 使用 DllPlugin + DllReferencePlugin 分離打包
    1. 項目代碼 分離打包
    2. 需要 dll 映射文件
  5. 配置緩存(插件自帶 loader,不支持的可以用 cache-loader

Webpack 加快代碼運行速度方法

  1. 代碼壓縮
  2. 抽離公共模塊
  3. 懶加載模塊
  4. 將小圖片轉成 base64 以減少請求
  5. 預取(prefetch) || 預加載(preload)
  6. 精靈圖
  7. webpack-bundle-analyzer 代碼分析

Webpack 優化細節

webpack 4.6.0+增加了對預取和預加載的支持。

動態導入

  import(/* webpackChunkName: "lodash" */ 'lodash')

  // 注釋中的使用webpackChunkName。
  // 這將導致我們單獨的包被命名,lodash.bundle.js
  // 而不是just [id].bundle.js。

預取(prefetch):將來可能需要一些導航資源

  • 只要父chunk加載完成,webpack就會添加 prefetch
  import(/* webpackPrefetch: true */ 'LoginModal');

  // 將<link rel="prefetch" href="login-modal-chunk.js">其附加在頁面的開頭

預加載(preload):當前導航期間可能需要資源

  • preload chunk 會在父 chunk 加載時,以并行方式開始加載
  • 不正確地使用 webpackPreload 會有損性能,
  import(/* webpackPreload: true */ 'ChartingLibrary');

  // 在加載父 chunk 的同時
  // 還會通過 <link rel="preload"> 請求 charting-library-chunk
DllPlugin + DllReferencePlugin

為了極大減少構建時間,進行分離打包。

DllReferencePlugin 和 DLL插件DllPlugin 都是在另外的 webpack 設置中使用的。

DllPlugin這個插件是在一個額外的獨立的 webpack 設置中創建一個只有 dll 的 bundle(dll-only-bundle)。 這個插件會生成一個名為 manifest.json 的文件,這個文件是用來讓 DLLReferencePlugin 映射到相關的依賴上去的。

webpack.vendor.config.js

  new webpack.DllPlugin({
    context: __dirname,
    name: "[name]_[hash]",
    path: path.join(__dirname, "manifest.json"),
  })

webpack.app.config.js

  new webpack.DllReferencePlugin({
    context: __dirname,
    manifest: require("./manifest.json"),
    name: "./my-dll.js",
    scope: "xyz",
    sourceType: "commonjs2"
  })
CommonsChunkPlugin

通過將公共模塊拆出來,最終合成的文件能夠在最開始的時候加載一次,便存到緩存中供后續使用。這個帶來速度上的提升,因為瀏覽器會迅速將公共的代碼從緩存中取出來,而不是每次訪問一個新頁面時,再去加載一個更大的文件。

如果把公共文件提取出一個文件,那么當用戶訪問了一個網頁,加載了這個公共文件,再訪問其他依賴公共文件的網頁時,就直接使用文件在瀏覽器的緩存,這樣公共文件就只用被傳輸一次。

  entry: {
    vendor: ["jquery", "other-lib"], // 明確第三方庫
    app: "./entry"
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      // filename: "vendor.js"
      // (給 chunk 一個不同的名字)

      minChunks: Infinity,
      // (隨著 entry chunk 越來越多,
      // 這個配置保證沒其它的模塊會打包進 vendor chunk)
    })
  ]

  // 打包后的文件
  <script src="vendor.js" charset="utf-8"></script>
  <script src="app.js" charset="utf-8"></script>
UglifyJSPlugin

基本上腳手架都包含了該插件,該插件會分析JS代碼語法樹,理解代碼的含義,從而做到去掉無效代碼、去掉日志輸入代碼、縮短變量名等優化。

  const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
  //...
  plugins: [
      new UglifyJSPlugin({
          compress: {
              warnings: false,  //刪除無用代碼時不輸出警告
              drop_console: true,  //刪除所有console語句,可以兼容IE
              collapse_vars: true,  //內嵌已定義但只使用一次的變量
              reduce_vars: true,  //提取使用多次但沒定義的靜態值到變量
          },
          output: {
              beautify: false, //最緊湊的輸出,不保留空格和制表符
              comments: false, //刪除所有注釋
          }
      })
  ]
ExtractTextPlugin + PurifyCSSPlugin

ExtractTextPlugin 從 bundle 中提取文本(CSS)到單獨的文件,PurifyCSSPlugin純化CSS(其實用處沒多大)

  module.exports = {
    module: {
      rules: [
        {
          test: /\.css$/,
          loader: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: [
              {
                loader: 'css-loader',
                options: {
                  localIdentName: 'purify_[hash:base64:5]',
                  modules: true
                }
              }
            ]
          })
        }
      ]
    },
    plugins: [
      ...,
      new PurifyCSSPlugin({
        purifyOptions: {
          whitelist: ['*purify*']
        }
      })
    ]
  };
DefinePlugin

DefinePlugin能夠自動檢測環境變化,效率高效。

在前端開發中,在不同的應用環境中,需要不同的配置。如:開發環境的API Mocker、測試流程中的數據偽造、打印調試信息。如果使用人工處理這些配置信息,不僅麻煩,而且容易出錯。

使用DefinePlugin配置的全局常量

注意,因為這個插件直接執行文本替換,給定的值必須包含字符串本身內的實際引號。通常,有兩種方式來達到這個效果,使用 ' "production" ', 或者使用 JSON.stringify('production')

    new webpack.DefinePlugin({

        // 當然,在運行node服務器的時候就應該按環境來配置文件
        // 下面模擬的測試環境運行配置

        'process.env':JSON.stringify('dev'),
        WP_CONF: JSON.stringify('dev'),
    }),

測試DefinePlugin:編寫

    if (WP_CONF === 'dev') {
        console.log('This is dev');
    } else {
        console.log('This is prod');
    }

打包后WP_CONF === 'dev'會編譯為false

    if (false) {
        console.log('This is dev');
    } else {
        console.log('This is prod');
    }
清除不可達代碼

當使用了DefinePlugin插件后,打包后的代碼會有很多冗余。可以通過UglifyJsPlugin清除不可達代碼

    [
        new UglifyJsPlugin({
            uglifyOptions: {
            compress: {
                warnings: false, // 去除warning警告
                dead_code: true, // 去除不可達代碼
            },
            warnings: false
            }
        })
    ]

最后的打包打包代碼會變成console.log('This is prod')

附Uglify文檔:https://github.com/mishoo/UglifyJS2

使用DefinePlugin區分環境 + UglifyJsPlugin清除不可達代碼,以減輕打包代碼體積

HappyPack

HappyPack可以開啟多進程Loader轉換,將任務分解給多個子進程,最后將結果發給主進程。

使用

  exports.plugins = [
    new HappyPack({
      id: 'jsx',
      threads: 4,
      loaders: [ 'babel-loader' ]
    }),

    new HappyPack({
      id: 'styles',
      threads: 2,
      loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
    })
  ];

  exports.module.rules = [
    {
      test: /\.js$/,
      use: 'happypack/loader?id=jsx'
    },

    {
      test: /\.less$/,
      use: 'happypack/loader?id=styles'
    },
  ]
ParallelUglifyPlugin

ParallelUglifyPlugin可以開啟多進程壓縮JS文件

  import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';

  module.exports = {
    plugins: [
      new ParallelUglifyPlugin({
        test,
        include,
        exclude,
        cacheDir,
        workerCount,
        sourceMap,
        uglifyJS: {
        },
        uglifyES: {
        }
      }),
    ],
  };
BundleAnalyzerPlugin

webpack打包結果分析插件

  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
  module.exports = {
    plugins: [
      new BundleAnalyzerPlugin()
    ]
  }
test & include & exclude

減小文件搜索范圍,從而提升速度

示例

  {
    test: /\.css$/,
    include: [
      path.resolve(__dirname, "app/styles"),
      path.resolve(__dirname, "vendor/styles")
    ]
  }
外部擴展(externals)

這玩意不是插件,是wenpack的配置選項

externals 配置選項提供了「從輸出的 bundle 中排除依賴」的方法。相反,所創建的 bundle 依賴于那些存在于用戶環境(consumer's environment)中的依賴。此功能通常對 library 開發人員來說是最有用的,然而也會有各種各樣的應用程序用到它。

  entry: {
    entry: './src/main.js',
    vendor: ['vue', 'vue-router', 'vuex']
  },
  externals: {
    // 從輸出的 bundle 中排除 echarts 依賴
    echarts: 'echarts',
  }

Webpack HMR 原理解析

Hot Module Replacement(簡稱 HMR)

包含以下內容:

  1. 熱更新圖
  2. 熱更新步驟講解
image
第一步:webpack 對文件系統進行 watch 打包到內存中

webpack-dev-middleware 調用 webpack 的 api 對文件系統 watch,當文件發生改變后,webpack 重新對文件進行編譯打包,然后保存到內存中。

webpack 將 bundle.js 文件打包到了內存中,不生成文件的原因就在于訪問內存中的代碼比訪問文件系統中的文件更快,而且也減少了代碼寫入文件的開銷。

這一切都歸功于memory-fs,memory-fs 是 webpack-dev-middleware 的一個依賴庫,webpack-dev-middleware 將 webpack 原本的 outputFileSystem 替換成了MemoryFileSystem 實例,這樣代碼就將輸出到內存中。

webpack-dev-middleware 中該部分源碼如下:

  // compiler
  // webpack-dev-middleware/lib/Shared.js
  var isMemoryFs = !compiler.compilers &&
                  compiler.outputFileSystem instanceof MemoryFileSystem;
  if(isMemoryFs) {
      fs = compiler.outputFileSystem;
  } else {
      fs = compiler.outputFileSystem = new MemoryFileSystem();
  }
第二步:devServer 通知瀏覽器端文件發生改變

在啟動 devServer 的時候,sockjs 在服務端和瀏覽器端建立了一個 webSocket 長連接,以便將 webpack 編譯和打包的各個階段狀態告知瀏覽器,最關鍵的步驟還是 webpack-dev-server 調用 webpack api 監聽 compile的 done 事件,當compile 完成后,webpack-dev-server通過 _sendStatus 方法將編譯打包后的新模塊 hash 值發送到瀏覽器端。

  // webpack-dev-server/lib/Server.js
  compiler.plugin('done', (stats) => {
    // stats.hash 是最新打包文件的 hash 值
    this._sendStats(this.sockets, stats.toJson(clientStats));
    this._stats = stats;
  });
  ...
  Server.prototype._sendStats = function (sockets, stats, force) {
    if (!force && stats &&
    (!stats.errors || stats.errors.length === 0) && stats.assets &&
    stats.assets.every(asset => !asset.emitted)
    ) { return this.sockWrite(sockets, 'still-ok'); }
    // 調用 sockWrite 方法將 hash 值通過 websocket 發送到瀏覽器端
    this.sockWrite(sockets, 'hash', stats.hash);
    if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); } 
    else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); }      else { this.sockWrite(sockets, 'ok'); }
  };
第三步:webpack-dev-server/client 接收到服務端消息做出響應

webpack-dev-server 修改了webpack 配置中的 entry 屬性,在里面添加了 webpack-dev-client 的代碼,這樣在最后的 bundle.js 文件中就會接收 websocket 消息的代碼了。

webpack-dev-server/client 當接收到 type 為 hash 消息后會將 hash 值暫存起來,當接收到 type 為 ok 的消息后對應用執行 reload 操作。

在 reload 操作中,webpack-dev-server/client 會根據 hot 配置決定是刷新瀏覽器還是對代碼進行熱更新(HMR)。代碼如下:

  // webpack-dev-server/client/index.js
  hash: function msgHash(hash) {
      currentHash = hash;
  },
  ok: function msgOk() {
      // ...
      reloadApp();
  },
  // ...
  function reloadApp() {
    // ...
    if (hot) {
      log.info('[WDS] App hot update...');
      const hotEmitter = require('webpack/hot/emitter');
      hotEmitter.emit('webpackHotUpdate', currentHash);
      // ...
    } else {
      log.info('[WDS] App updated. Reloading...');
      self.location.reload();
    }
  }
第四步:webpack 接收到最新 hash 值驗證并請求模塊代碼

首先 webpack/hot/dev-server(以下簡稱 dev-server) 監聽第三步 webpack-dev-server/client 發送的 webpackHotUpdate 消息,調用 webpack/lib/HotModuleReplacement.runtime(簡稱 HMR runtime)中的 check 方法,檢測是否有新的更新。

在 check 過程中會利用 webpack/lib/JsonpMainTemplate.runtime(簡稱 jsonp runtime)中的兩個方法 hotDownloadManifest 和 hotDownloadUpdateChunk。

hotDownloadManifest 是調用 AJAX 向服務端請求是否有更新的文件,如果有將發更新的文件列表返回瀏覽器端。該方法返回的是最新的 hash 值。

hotDownloadUpdateChunk 是通過 jsonp 請求最新的模塊代碼,然后將代碼返回給 HMR runtime,HMR runtime 會根據返回的新模塊代碼做進一步處理,可能是刷新頁面,也可能是對模塊進行熱更新。該 方法返回的就是最新 hash 值對應的代碼塊。

最后將新的代碼塊返回給 HMR runtime,進行模塊熱更新。

附:為什么更新模塊的代碼不直接在第三步通過 websocket 發送到瀏覽器端,而是通過 jsonp 來獲取呢?

我的理解是,功能塊的解耦,各個模塊各司其職,dev-server/client 只負責消息的傳遞而不負責新模塊的獲取,而這些工作應該有 HMR runtime 來完成,HMR runtime 才應該是獲取新代碼的地方。再就是因為不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware 和 webpack 配合也可以完成模塊熱更新流程,在使用 webpack-hot-middleware 中有件有意思的事,它沒有使用 websocket,而是使用的 EventSource。綜上所述,HMR 的工作流中,不應該把新模塊代碼放在 websocket 消息中。

第五步:HotModuleReplacement.runtime 對模塊進行熱更新

這一步是整個模塊熱更新(HMR)的關鍵步驟,而且模塊熱更新都是發生在HMR runtime 中的 hotApply 方法中

  // webpack/lib/HotModuleReplacement.runtime
  function hotApply() {
      // ...
      var idx;
      var queue = outdatedModules.slice();
      while(queue.length > 0) {
          moduleId = queue.pop();
          module = installedModules[moduleId];
          // ...
          // remove module from cache
          delete installedModules[moduleId];
          // when disposing there is no need to call dispose handler
          delete outdatedDependencies[moduleId];
          // remove "parents" references from all children
          for(j = 0; j < module.children.length; j++) {
              var child = installedModules[module.children[j]];
              if(!child) continue;
              idx = child.parents.indexOf(moduleId);
              if(idx >= 0) {
                  child.parents.splice(idx, 1);
              }
          }
      }
      // ...
      // insert new code
      for(moduleId in appliedUpdate) {
          if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
              modules[moduleId] = appliedUpdate[moduleId];
          }
      }
      // ...
  }

模塊熱更新的錯誤處理,如果在熱更新過程中出現錯誤,熱更新將回退到刷新瀏覽器,這部分代碼在 dev-server 代碼中,簡要代碼如下:

  module.hot.check(true).then(function(updatedModules) {
    if(!updatedModules) {
        return window.location.reload();
    }
    // ...
  }).catch(function(err) {
      var status = module.hot.status();
      if(["abort", "fail"].indexOf(status) >= 0) {
          window.location.reload();
      }
  });
第六步:業務代碼需要做些什么?

當用新的模塊代碼替換老的模塊后,但是我們的業務代碼并不能知道代碼已經發生變化,也就是說,當 hello.js 文件修改后,我們需要在 index.js 文件中調用 HMR 的 accept 方法,添加模塊更新后的處理函數,及時將 hello 方法的返回值插入到頁面中。代碼如下

  // index.js
  if(module.hot) {
      module.hot.accept('./hello.js', function() {
          div.innerHTML = hello()
      })
  }

最后

  1. 覺得有用的請點個贊
  2. 本文內容出自 https://github.com/zhongmeizhi/FED-note
  3. 歡迎關注公眾號「前端進階課」認真學前端,一起進階。
image
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380