webpack輸出文件分析以及編寫一個loader

webpack構建流程

webpack是時下最流行的前端打包構建工具瘪匿,本質上是一個模塊打包器暇检,通過從入口文件開始遞歸的分析尋找模塊之間的依賴嗽仪,最終輸出一個或多個bundle文件锡垄。

webpack的構建是一個串行的流程破镰,從啟動到結束,會依次執(zhí)行以下流程:

  1. 初始化配置

    從配置文件和命令行中讀取參數并合并參數默辨,生成最終的配置項德频,并且執(zhí)行配置文件中的插件實例化語句,生成Compiler傳入plugin的apply方法缩幸,為webpack事件流掛上自定義鉤子抱婉;

  2. 開始編譯

    生成compiler示例档叔,執(zhí)行compiler.run開始編譯;

  3. 確定入口文件

    從配置項中讀取所有的入口文件蒸绩;

  4. 編譯模塊

    從入口文件開始編譯衙四,使用對應的loader編譯模塊,并且遞歸的編譯當前模塊所依賴的模塊患亿,在所有的模塊都編譯完成后传蹈,得到所有模塊的最終內容和模塊之間的依賴關系,最后將所有模塊的 require 語句替換為 __webpack_require__ 來模擬模塊化操作步藕;

  5. 資源輸出

    根據入口和模塊的依賴關系惦界,組裝成一個個包含多個模塊的chunk,然后將chunk轉換成一個單獨的文件加入輸出列表咙冗。

  6. 生成文件

    將生成的內容根據配置生成文件沾歪,輸出到指定的位置。

webpack的核心對象是Compile雾消,負責文件的監(jiān)聽和啟動編譯灾搏,繼承自Tapable[https://github.com/webpack/tapable],使得Compile實例具備了注冊和調用插件的功能立润。

在webpack執(zhí)行構建流程時狂窑,webpack會在特定的時機廣播對應的事件,插件在監(jiān)聽到事件后桑腮,會執(zhí)行特定的邏輯來修改模塊的內容泉哈。

通過下面這個流程圖我們能夠對webpack的構建流程有個更直觀的印象:

process.png

<center>webpack運行流程圖</center >

webpack輸出文件分析

下面,我們將通過分析webpack輸出的bundle文件破讨,了解bundle文件是如何在瀏覽器中運行的丛晦。

單文件分析

首先創(chuàng)建 src/index.js ,執(zhí)行一個最簡單的js語句:

console.log('hello world')

創(chuàng)建 webpack.config.js提陶, 配置如下:

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist')
  }
}

本例中使用的webpack版本為4.35.3烫沙,此處為了更好的分析輸出的bundle文件,將mode設置為'none'搁骑,此時webpack不會默認啟用任何插件。

mode有三個可選值又固,分別是'none'仲器、'production'、'development'仰冠,默認值為'production'乏冀,默認開啟以下插件:

  • FlagDependencyUsagePlugin:編譯時標記依賴;
  • FlagIncludedChunksPlugin:標記子chunks洋只,防止多次加載依賴辆沦;
  • ModuleConcatenationPlugin:作用域提升(scope hosting)昼捍,預編譯功能,提升或者預編譯所有模塊到一個閉包中肢扯,提升代碼在瀏覽器中的執(zhí)行速度妒茬;
  • NoEmitOnErrorsPlugin:在輸出階段時,遇到編譯錯誤跳過蔚晨;
  • OccurrenceOrderPlugin:給經常使用的ids更短的值乍钻;
  • SideEffectsFlagPlugin:識別 package.json 或者 module.rules 的 sideEffects 標志(純的 ES2015 模塊),安全地刪除未用到的 export 導出铭腕;
  • TerserPlugin:壓縮代碼

mode值為'development'時银择,默認開啟以下插件:

  • NamedChunksPlugin:以名稱固化chunkId;
  • NamedModulesPlugin:以名稱固化moduleId

執(zhí)行webpack構建命令:

$ webpack

輸出到dist文件夾中的 main.js 文件內容如下:

(function(modules) { // webpackBootstrap
// 模塊緩存
var installedModules = {};

// 模塊加載函數
function __webpack_require__(moduleId) {

  // 如果加載過該模塊累舷,則直接從緩存中讀取
  if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
  }
  // 創(chuàng)建新模塊并將其緩存起來
  var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
  };

  // 執(zhí)行模塊函數浩考,設置module.exports
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

  // 將module標記為已加載
  module.l = true;

  // 返回設置好的module.exports
  return module.exports;
}


// 指向modules
__webpack_require__.m = modules;

// 指向緩存
__webpack_require__.c = installedModules;

// 定義exports的get方式
__webpack_require__.d = function(exports, name, getter) {
  if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
  }
};

// 設置es6模塊標記
__webpack_require__.r = function(exports) {
  if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  }
  Object.defineProperty(exports, '__esModule', { value: true });
};

// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
  if(mode & 1) value = __webpack_require__(value);
  if(mode & 8) return value;
  if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
  var ns = Object.create(null);
  __webpack_require__.r(ns);
  Object.defineProperty(ns, 'default', { enumerable: true, value: value });
  if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
  return ns;
};

// 兼容commonjs和es6模塊
__webpack_require__.n = function(module) {
  var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
  __webpack_require__.d(getter, 'a', getter);
  return getter;
};

// Object.prototype.hasOwnProperty的封裝
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

// webpack配置的publicpath
__webpack_require__.p = "";


// 加載模塊并返回
return __webpack_require__(__webpack_require__.s = 0);
})
/************************************************************************/
([
/* 0 */
/***/ (function(module, exports) {

console.log('hello world')

/***/ })
]);

可以看到輸出的代碼是個IIFE(立即執(zhí)行函數),可以簡化如下:

(function(modules) {
  var installedModules = {};

  // webpack require語句
  // 加載模塊
  function __webpack_require__(moduleId) {}

  return __webpack_require__(0)
})([
  function(module, exports) {
    console.log('hello world')
  }
])

簡化后代碼中的 __webpack_require__ 函數起到的就是加載模塊的功能被盈,IIFE函數接收的參數是個數組析孽,第0項內容便是 src/index.js 中的代碼語句,通過 __webpack_require__ 函數加載并執(zhí)行模塊害捕,最終在瀏覽器控制臺輸出 hello world绿淋。

接下來我們通過代碼分析下 __webpack_reuqire__ 函數內部是如何工作的

function __weboack_require__(moduleId) {
  // 如果已經加載過該模塊,則從緩存中直接讀取
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }

  // 如果沒有加載過該模塊尝盼,則創(chuàng)建一個新的module存入緩存中
  var module = installedModules[moduleId] = {
    i: moduleId, // module id
    l: false, // 是否已加載 false
    exports: {} // 模塊導出
  };

  // 執(zhí)行該module
  // call方法第一個參數為modules.exports吞滞,是為了module內部的this指向該模塊
  // 然后傳入三個參數,分別為module, module.exports, __webpack_require__模塊加載函數
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

  // 設置module為已加載
  module.l = true;

  // 最終返回module.exports
  return module.exports;
  }
}

可以看到 __webpack_require__ 函數接收一個模塊id盾沫,通過執(zhí)行該模塊裁赠,最終返回該模塊的exports,并將模塊緩存在內存中赴精。如果再次加載該模塊佩捞, 則直接從緩存中讀取。 modules[modulesId] 的內容是IIFE參數的第0項蕾哟,即:

function(module, exports) {
  console.log('hello world')
}

在導出的IIFE中一忱,除了 __webpack_require__ 函數,還在 __webpack_require__ 下掛載了很多屬性.

  • __webpack_require__.m : 掛載所有的modules谭确;
  • __webpack_require__.c : 掛載已緩存的modules帘营;
  • __webpack_require__.d : 定義exports的getter;
  • __webpack_require__.r : 將module設置為es6模塊逐哈;
  • __webpack_require__.t : 根據不同的場景返回對應處理后的模塊或值芬迄;
  • __webpack_require__.n : 返回getter,內部區(qū)分是否為es6模塊昂秃;
  • __webpack_require__.o : Object.prototype.hasOwnProperty功能封裝禀梳;
  • __webpack_require__.p : output配置項中的publicPath屬性杜窄;

多文件引用分析

在前面的例子中,webpack打包的bundle中只包含一個非常簡單的入口文件算途,并不存在模塊之間的引用塞耕。

下面我們修改下 src/index.js 中的代碼,引用一個ES6模塊 src/math.js 進來:

// math.js
const add = function (a, b) {
  return a + b
}

export default add
// index.js
import add from './math'

console.log(add(1, 2))

重新執(zhí)行webpack打包命令郊艘,可以看到輸出的IIFE中的參數已經變成了兩項:

([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


console.log(Object(_math__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2))


/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
const add = function (a, b) {
  return a + b
}

/* harmony default export */ __webpack_exports__["default"] = (add);


/***/ })
 ]);

數組第1項中定義了 math.js 模塊荷科,并且通過執(zhí)行 __webpack_require__.r(__webpack_exports__) 使得webpack能夠識別出該模塊是個ES6模塊,最后將 __webpack_exports__default 屬性值設置為函數 add 纱注。

數組第0項是 index.js 打包后輸出的模塊畏浆,語句 var _math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1) 的功能即是將模塊 math.js 導出的 add 函數引進來, __webpack_require__(1) 返回 module.exports狞贱,其中 1 是由webpack在打包時生成的chunkId刻获,最后通過 console.log(Object(_math__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2)) 執(zhí)行 index.js 中的語句。

webpack通過將原本獨立的一個個模塊存放到IIFE的參數中來加載瞎嬉,從而達到只進行一次網絡請求便可執(zhí)行所有模塊蝎毡,避免了通過多次網絡加載各個模塊造成的加載時間過長的問題。并且在IIFE函數內部氧枣,webpack也對模塊的加載做了進一步優(yōu)化沐兵,通過將已經加載過的模塊緩存起來存在內存中,第二次加載相同模塊時便直接從內存中取出便监。

異步加載分析

上面兩個例子都是同步加載模塊并執(zhí)行扎谎,但是在實際項目中為了提高頁面的加載速度,往往對首屏初始化時暫時用不到的模塊進行異步加載烧董,比如從首頁跳轉后的路由模塊等毁靶。接下來我們將通過異步加載的方式來加載 math.js 模塊并執(zhí)行其導出的 add 函數。

import('./math').then((add) => {
  console.log(add(1, 2))
})

重新打包后逊移,輸出 main.js1.js预吆,1.js 是需要異步加載的文件。

先分析入口文件 main.js 胳泉,可以看到相對于同步加載方式的代碼輸出拐叉,文件中多了 __webpack_require__.ewebpackJsonpCallback 函數,IIFE中的參數也只有一個:

/***/ (function(module, exports, __webpack_require__) {


__webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then((add) => {
  console.log(add(1, 2))
})


/***/ })

該模塊通過 __webpack_require__.e(1) 的方式加載模塊1的文件扇商,加載成功后再通過執(zhí)行 __webpack_require__.bind(null, 1) 返回模塊1凤瘦,然后執(zhí)行該模塊導出的 add 函數。

__webpack_require__.e 的作用便是加載需要異步加載的模塊钳吟,函數的內容如下:

__webpack_require__.e = function requireEnsure(chunkId) {
 var promises = [];

 var installedChunkData = installedChunks[chunkId];
 if (installedChunkData !== 0) { // 如果為0則代表已經加載過該模塊

   // installedChunkData 不為空且不為0表示該 Chunk 正在網絡加載中
   // 直接返回promise對象
   if (installedChunkData) {
     promises.push(installedChunkData[2]);
   } else {
     // 該chunk從未被加載過廷粒,返回數組包含三項窘拯,分別是resolve红且,reject和創(chuàng)建的promise對象
     var promise = new Promise(function (resolve, reject) {
       installedChunkData = installedChunks[chunkId] = [resolve, reject];
     });
     promises.push(installedChunkData[2] = promise);

     // 創(chuàng)建script標簽坝茎,加載模塊
     var script = document.createElement('script');
     var onScriptComplete;

     script.charset = 'utf-8';
     script.timeout = 120;
     if (__webpack_require__.nc) {
       script.setAttribute("nonce", __webpack_require__.nc);
     }

     // jsonpScriptSrc的作用是返回根據配置的publicPath和chunkId生成的文件路徑
     script.src = jsonpScriptSrc(chunkId);

     // 創(chuàng)建一個Error實例,用于在加載錯誤時catch
     var error = new Error();
     onScriptComplete = function (event) {
       // 防止內存泄漏
       script.onerror = script.onload = null;
       clearTimeout(timeout);
       var chunk = installedChunks[chunkId];

       if (chunk !== 0) {
         if (chunk) {
           // chunk加載失敗暇番,拋出錯誤
           var errorType = event && (event.type === 'load' ? 'missing' : event.type);
           var realSrc = event && event.target && event.target.src;
           error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
           error.name = 'ChunkLoadError';
           error.type = errorType;
           error.request = realSrc;
           chunk[1](error);
         }
         installedChunks[chunkId] = undefined;
       }
     };

     // 異步加載最長等待時間120s
     var timeout = setTimeout(function () {
       onScriptComplete({ type: 'timeout', target: script });
     }, 120000);
     script.onerror = script.onload = onScriptComplete;

     // 將創(chuàng)建的script標簽插入dom中
     document.head.appendChild(script);
   }
 }
 return Promise.all(promises);
};

函數內部先判斷是否加載過該模塊嗤放,如果沒有加載過,則創(chuàng)建一個script標簽壁酬,script的路徑是通過內部的 jsonpScriptSrc 函數根據webpack的配置生成最終的src路徑返回得到次酌。函數最終返回一個 Promise 對象,js文件加載失敗時則會執(zhí)行 reject將錯誤拋出舆乔。

math.js 輸出的bundle 1.js 的內容很簡單岳服,代碼如下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
/* 0 */,
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
const add = function (a, b) {
  return a + b
}

/* harmony default export */ __webpack_exports__["default"] = (add);


/***/ })
]]);

可以看到該bundle的作用就是向 window['webpackJsonp'] 數組中push了一個新的數組,其中第一項 [1] 是webpack生成的chunkId希俩,第二項是 math.js 轉換后的模塊具體內容吊宋。

與此同時,在 main.js 中IIFE的后部分颜武,對掛載在全局的 window['webpackJsonp'] 數組的push方法進行了重寫璃搜,指向了在前面定義過的 webpackJsonpCallback 函數:

function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];
    // 將data第1項模塊添加到modules中,
    // 然后將對應的chunkId標記為已加載
    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }

    // 將傳進來的moreModules數組中的每一個模塊依次添加到IIFE中緩存的modules中
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }

    // parentJsonpFunction為window['webpackJsonp']中原聲的數組push方法
    // 執(zhí)行parentJsonpFunction將data真正的添加到window['webpackJsonp']數組中去
    if(parentJsonpFunction) parentJsonpFunction(data);

    // 將前面創(chuàng)建的promise執(zhí)行resolve
    while(resolves.length) {
        resolves.shift()();
    }
};

通過分析 webpackJsonpCallback 函數的內容,可以看到該函數的主要作用是將傳入的chunkid標記為已加載鳞上,并將傳入的模塊掛在到緩存模塊的 modules 對象上这吻,最終執(zhí)行 __webpack_require__.e 函數返回的promise對象的resolve方法代表該異步加載的模塊已經加載完成,此時篙议,在 __webpack_require__.e(1).then() 中便可以通過同步加載模塊的方式加載該模塊啦唾糯。

重新梳理一下入口主文件加載異步模塊的大概流程:

  1. 執(zhí)行 __webpack_require__.e 加載異步模塊;

  2. 創(chuàng)建chunkid對應的script標簽加載腳本涡上,并返回promise趾断;

  3. 如果加載失敗,reject掉promise吩愧;如果加載成功芋酌,異步chunk立即執(zhí)行 window[webpackJsonp] 的push方法,將模塊標記為已加載雁佳,并resolve掉相應的promise脐帝;

  4. 成功后可在 __webpack_require__.e().then 中以同步的方式加載模塊。

輸出文件總結

在webpack輸出的文件中糖权,通過IIFE的形式將所有模塊作為參數都傳遞進來堵腹,用 __webpack_require__ 模擬import或者require語句,然后從入口模塊開始依次遞歸的執(zhí)行加載模塊星澳,需要異步加載的模塊疚顷,通過在dom上插入一個新的script標簽加載。并且內部對模塊加載做了緩存處理優(yōu)化。

在實際的項目中腿堤,輸出的bundle內容會遠比本文中的demo復雜的多阀坏,并且會有chunkId設置,公共chunk抽取笆檀,代碼壓縮混淆等優(yōu)化忌堂,但是可以通過這個最基本的demo,熟悉webpack輸出的文件在運行時的工作流程酗洒,便于我們在調試時更好的分析士修。

編寫一個簡單的loader

在編寫一個loader之前,先簡單介紹下webpack loader的作用樱衷。在webpack中棋嘲,可以將loader理解為一個轉換器,通過處理文件的輸入矩桂,返回一個新的結果封字,最終交給webpack進行下一步的處理。

一個loader就是一個nodejs模塊耍鬓,它的基本結構如下:

// 可以通過loader-utils這個包獲取該loader的配置項options
const loaderUtils = require('loader-utils')

// 導出一個函數阔籽,source為webpack傳遞給loader的文件源內容
module.exports = function(source) {
  // 獲取該loader的配置項
  const options = loaderUtils.getOptions(this)

  // 一些轉換處理,最終返回處理后的結果牲蜀。
  return source
}

在平時配置webpack loader的時候笆制,都是使用通過npm安裝的loader,為了加載本地的loader涣达,一般有兩種方式在辆,第一種是通過npm link的方式將loader關聯到項目的node_modules下,還有一種方式是通過配置wepack的resolveLoader.modules配置項度苔,告訴webpack通過何種形式尋找loader匆篓。第一種方式需要配置相關的 package.json ,在本例中使用第二種方式配置寇窑。

module.exports = {
  resolveLoader: {
    // 假設本地編寫的loader在loaders文件夾下
    modules: ['node_modules', './loaders/']
  }
}

下面我們編寫一個loader鸦概,用于刪除代碼中的注釋。命名為remove-comment-loader:


module.exports = function(source) {
  // 匹配js中的注釋內容
  const reg = new RegExp(/(\/\/.*)|(\/\*[\s\S]*?\*\/)/g)

  // 刪除注釋
  return source.replace(reg, '')
}

然后修改webpack.config.js:

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'remove-comment-loader' // 當匹配到js文件時甩骏,使用我們編寫的remove-comment-loader
      }
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', './loaders/'] // 配置加載本地loader
  }
}

然后在入口文件代碼中加上一些注釋窗市,重新打包查看輸出文件,就能看到代碼中的注釋已經被刪除了饮笛。

本文中的demo代碼參見咨察;https://github.com/duwenbin0316/webpack-runtime-demo

在此處順便向大家推薦下民生科技公司Firefly移動金融開發(fā)平臺中的前端打包構建工具apollo-build。apollo-build包含開發(fā)調試福青、打包摄狱、測試脓诡、
和打包dll的功能,并且提供了非常好用的前端接口Mock功能媒役,命令行體驗和create-react-app一致誉券。我們封裝了webpack中的大部分常用功能并在內部做了很多優(yōu)化,從中提取出了最常用的配置項刊愚,即使不熟悉webpack的配置也能快速上手,并且也支持通過 webpack.config.js 的方式做高階的修改踩验,歡迎訪問民生科技官網了解鸥诽。

參考

<hr />

《深入淺出webpack》 - 吳浩麟

Webpack揭秘——走向高階前端的必經之路

作者介紹

杜文斌

民生科技有限公司用戶體驗技術部Firefly移動金融開發(fā)平臺前端開發(fā)工程師

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市箕憾,隨后出現的幾起案子牡借,更是在濱河造成了極大的恐慌,老刑警劉巖袭异,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钠龙,死亡現場離奇詭異,居然都是意外死亡御铃,警方通過查閱死者的電腦和手機碴里,發(fā)現死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來上真,“玉大人咬腋,你說我怎么就攤上這事∷ィ” “怎么了根竿?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長就珠。 經常有香客問我寇壳,道長,這世上最難降的妖魔是什么妻怎? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任壳炎,我火速辦了婚禮,結果婚禮上逼侦,老公的妹妹穿的比我還像新娘冕广。我一直安慰自己,他們只是感情好偿洁,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布撒汉。 她就那樣靜靜地躺著,像睡著了一般涕滋。 火紅的嫁衣襯著肌膚如雪睬辐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天,我揣著相機與錄音溯饵,去河邊找鬼侵俗。 笑死,一個胖子當著我的面吹牛丰刊,可吹牛的內容都是我干的隘谣。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼啄巧,長吁一口氣:“原來是場噩夢啊……” “哼寻歧!你這毒婦竟也來了?” 一聲冷哼從身側響起秩仆,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤码泛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后澄耍,有當地人在樹林里發(fā)現了一具尸體噪珊,經...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年齐莲,在試婚紗的時候發(fā)現自己被綠了痢站。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡选酗,死狀恐怖瑟押,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情星掰,我是刑警寧澤多望,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站氢烘,受9級特大地震影響怀偷,放射性物質發(fā)生泄漏。R本人自食惡果不足惜播玖,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一椎工、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蜀踏,春花似錦维蒙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至局待,卻和暖如春斑响,著一層夾襖步出監(jiān)牢的瞬間菱属,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工舰罚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留纽门,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓营罢,卻偏偏與公主長得像赏陵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子饲漾,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內容