webpack 之 Loader 詳解

對于webpack凛俱,一切皆模塊。webpack 只能理解 JavaScript 和 JSON 文件料饥,其他類型/后綴的文件都需要經過 loader 處理蒲犬,將它們轉換為js可識別的有效模塊 (webpack 天生支持 ECMAScript、CommonJS岸啡、資源模塊等模塊類型)原叮。loader可以做語言翻譯(比如將文件從 TypeScript 轉換為 JavaScript) 或格式轉換(將內聯(lián)圖像轉換為 data URL)還有樣式編譯(允許直接在 JavaScript 模塊中 import CSS文件)。

loader 是什么

每個 loader 本質上都是一個導出為函數的 JavaScript 模塊。loader runner 會調用此函數奋隶,將資源文件或者上一個 loader 產生的結果傳進去擂送,經過編譯轉換把處理結果再輸出去(如果后面還有 loader 就傳給下一個)。函數中的 this 作為上下文會被 webpack 填充唯欣,并且 loader runner 中包含一些實用的方法嘹吨,比如可以使 loader 調用方式變?yōu)楫惒剑蛘攉@取 query 參數境氢。
簡言之 loader 就是模塊轉換器蟀拷。有點像 Vue 的過濾器。

同步模式

loader 如果返回單個處理結果萍聊,可以在直接 return问芬。如果有多個處理結果,則必須調用 this.callback()寿桨。this.callback 方法則更靈活此衅,因為它允許傳遞多個參數,而不僅僅是 content亭螟。

module.exports = function (content, map, meta) {
  return someSyncOperation(content);
};
// 需要傳遞多個參數炕柔,用 this.callback
module.exports = function (content, map, meta) {
  this.callback(null, someSyncOperation(content), map, meta);
  return; // 當調用 callback() 函數時,總是返回 undefined
};

異步模式

由于同步計算過于耗時媒佣,在 Node.js 這樣的單線程環(huán)境下進行此操作并不是好的方案,很多 loader 都是異步的陵刹。
在異步 loader 中默伍,必須調用 this.async() 來告知 loader runner 等待異步結果,它會返回 this.callback() 回調函數衰琐。隨后 loader 必須返回 undefined 并且調用該回調函數也糊。

module.exports = function (content, map, meta) {
  var callback = this.async();
  someAsyncOperation(content, function (err, result) {
    if (err) return callback(err);
    callback(null, result, map, meta);
  });
};
// 多個處理結果
module.exports = function (content, map, meta) {
  var callback = this.async();
  someAsyncOperation(content, function (err, result, sourceMaps, meta) {
    if (err) return callback(err);
    callback(null, result, sourceMaps, meta);
  });
};

以下是一個模擬的raw-loader,它用于加載文件的原始內容(utf-8)羡宙。比如把一個 import/require() 進來的 txt文件的內容以字符串的形式導出去狸剃。這個 loader 雖然在 webpack5 已經棄用了,但我們仍可以參考下自定義 loader 的寫法狗热。

// 獲取webpack配置options參數的方法钞馁,寫loader的固定第一步
const { getOptions } = require("loader-utils");
/**
 *
 * @param {string|Buffer} content 傳入的源文件的內容
 * @param {object} [map] 可以被 https://github.com/mozilla/source-map 使用的 SourceMap 數據
 * @param {any} [meta] meta 數據,可以是任何內容
 */
module.exports = function(content, map, meta) {
  // 如果是module.rules配置匿刮,返回的是options僧凰;如果是內聯(lián)loadername!語法,返回根據query字符串生成的對象
  const opts = getOptions(this) || {};

  const code = JSON.stringify(content);
  const isESM = typeof opts.esModule !== "undefined" ? options.esModule : true;
  // 根據配置是否開啟esModule決定導出語句熟丸,直接返回原文件內容
  // esModule: false 就是使用 CommonJS 規(guī)范
  const result = `${isESM ? "export default" : "module.exports ="} ${code}`;
  return result;
};

比如我們有一個文本文件 example.txt训措,調用這個 loader 后,過程相當于:
源文件內容 this is a txt file
被處理成了 js:

// example.js
export default 'this is a txt file'
// 或
module.exports = 'this is a txt file';

然后 webpack 交給 require 去引入:

const source = require('example.js'); 
console.log(source); // this is a txt file

loader 的使用/配置

1. 在config文件配置

module.rules 配置轉換規(guī)則時,有兩個必選屬性 test 和 use绩鸣。
像這樣 module: { rules: [{ test: /\.txt$/, use: 'raw-loader' }] }
會告訴 webpack 編譯器(compiler) 怀大,當碰到「在 require()/import 語句中被解析為 '.txt' 的路徑」時,在你對它打包之前呀闻,先用 raw-loader 轉換一下(預處理)化借。

module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: ['a-loader', 'b-loader', 'c-loader'], // 從右到左,c-loader -> b-loader -> a-loader
      },
      {
        test: /\.css$/, // test屬性总珠,規(guī)定哪些文件會被轉換
        use: [ // use屬性屏鳍,在進行轉換時,應用哪些 loader
          { loader: 'style-loader' }, 
          {
            loader: 'css-loader',
            options: {
              modules: true
            }
          },
          { loader: 'sass-loader' } 
        ] // 從下到上局服,sass-loader -> css-loader -> style-loader
      }
    ],
  },
}
2. 用 webpack-chain 配置

end() 方法的作用是向上移動一級钓瞭,移回更高的上下文環(huán)境,就可以繼續(xù)鏈式調用上級API的方法淫奔。比如這里是到.use() 層級繼續(xù)鏈式調用

// 從下到上山涡,vue-loader -> cache-loader
webpackConfig.module
  .rule('vue')
    .test(/\.vue$/)
    .use('cache-loader')
      .loader('cache-loader')
      .options({...})
      .end()
    .use('vue-loader')
      .loader('vue-loader')
      .options(Object.assign({...})
3. 內聯(lián)方式(inline)(官方不推薦使用)

在 import 語句中顯式指定 loader。
使用 ! 將資源中的 loader 分開唆迁。每個部分都會相對于當前目錄解析鸭丛。對于解析相同的文件類型,inline loader優(yōu)先級高于config配置文件中的loader唐责。這個方法不用熟記(官方都說了不應該使用orz)鳞溉,了解即可。
通過為內聯(lián) import 語句添加前綴鼠哥,可以覆蓋 配置 中的所有 loader, preLoader 和 postLoader:

// 從右到左熟菲,css-loader -> style-loader
import Styles from 'style-loader!css-loader?modules!./styles.css'
// 相當于
rules: [{
    test: /^styles\.css$/, 
    use: [
      { loader: 'style-loader' }, 
      {
        loader: 'css-loader',
        options: {
          modules: true
        }
      },
    ] 
}]

loader 的執(zhí)行順序

每個 Loader 的功能都應是單一而專注的,這樣不僅便于維護朴恳,還能讓它們在更多場景中被串聯(lián)應用抄罕。因此 Loader 通常是組合使用的。鏈式調用一組 loader 時 (無論是模塊規(guī)則配置還是內聯(lián)方式)于颖,它們會按照相反的順序執(zhí)行呆贿。即從右到左(或從下到上),依次將前一個 loader 轉換后的結果傳遞給下一個 loader森渐。直到最后一個 loader 返回 webpack 所期望的 JavaScript做入。有點像 Promise 的 then。
loader 可以用 String 或 Buffer 的形式傳遞它的處理結果章母,complier 會把它們在 loader 之間相互轉換母蛛。最終結果也就是最后一個 loader 會返回一或兩個值:第一個是代表模塊的 JavaScript 源碼的 String 或者 Buffer(這個結果會交給 webpack 的 require,因此一定是一段可執(zhí)行的 node 模塊的 JS 腳本[用字符串存儲的])乳怎;第二個是可選的 SourceMap (格式為 JSON 對象)彩郊。

一組 loader 的執(zhí)行有兩個階段:Pitching 階段 和 Normal 階段前弯,類似于js中的事件捕獲、冒泡秫逝。
webpack 的 loader-runner 會按正序(從左到右) require 每個 loader恕出,把這個 loader 的模塊導出函數 和 pitch函數都存到 loaderContext 對象上,然后執(zhí)行該 loader 的 pitch 方法(如果有的話)违帆;如果一組 loader 的 pitch 都沒有返回值浙巫,就開始 Normal階段反向(從右到左)執(zhí)行 loader 的導出函數,依次進行模塊源碼的轉換刷后,直到拿到最后的處理結果的畴;但是當 Pitching 階段某個 loader 的 pitch 有返回值,那么就會跳過剩余未讀取的 loader尝胆,直接進入執(zhí)行 loader 的環(huán)節(jié)丧裁。從前一個 require 的 loader 開始執(zhí)行,pitch 的返回值即是傳入的第一個參數含衔。除了 pitch 有返回的那個 loader煎娇,倒序執(zhí)行已經 require 的每個 loader。
原理可參考:淺析 webpack 打包流程(原理) 二 之【執(zhí)行 loader 階段贪染,初始化模塊 module缓呛,并用 loader 倒序轉譯】部分

module: {
  rules: [{ test:/\.vue$/, use: ['a-loader', 'b-loader', 'c-loader'] }]
},

根據以上配置,*.vue文件在 loader 處理階段將經歷以下步驟:

|- a-loader `pitch` 方法
  |- b-loader `pitch` 方法
    |- c-loader `pitch` 方法
      |- 以模塊依賴的形式即 import/require() 獲取資源內容
    |- c-loader normal 執(zhí)行
  |- b-loader normal 執(zhí)行
|- a-loader normal 執(zhí)行

如果 b-loader 的 pitch 方法有返回值杭隙,直接跳過 c-loader 進入 loader 執(zhí)行階段哟绊,并且 b-loader 也不會執(zhí)行。整個過程就會變成這樣:

|- a-loader `pitch` 方法
  |- b-loader `pitch` 方法 (有返回結果痰憎,則跳過后面未 require 的 loader匿情,直接進入 loader 執(zhí)行階段)
|- a-loader normal 執(zhí)行 (傳入參數是 b-loader pitch 的返回值)

圖解更清晰:

loader 可以利用 pitch 階段來做什么?

Pitch 方法是什么:每個 loader 可以掛載一個 pitch 函數信殊,該函數主要用于利用 modulerequest 來提前做一些攔截處理的工作(后面會舉例說明),并不實際處理模塊內容汁果。
事實上很多 loader 并未定義 pitch涡拘,一般定義了 pitch 就是某些情況要返回東西。
詳情請看 Pitching Loader据德。

當一組 loader 被鏈式調用鳄乏,像上面的例子,正常情況只有最后一個c-loader能獲得資源文件(起始 loader 只有一個入參:資源文件的內容)棘利,b-loader拿到的是c-loader處理結果橱野,中間如果再多幾個 loader 也是如此,只能拿到上一個傳來的值善玫,處理好再傳遞給下一個水援。直到第一個a-loader返回最終結果。
盡管 loaders 常被串聯(lián)使用,但它們的功能仍舊是單一并獨立的蜗元,且只關心自己的輸入和輸出或渤。就像工廠流水線,一個區(qū)域的工人/機器只干一種類型的活奕扣。所以合理搭配并配置正確的順序才能得到我們想要的結果薪鹦。

它只想要 request 后面的 元數據(調用 loader時傳入的第三個參數 metadata)。
但有時候我們需要把兩個用來做最后處理的 loader 串起來惯豆,比如 style-loader 和 css-loader池磁。
但 style-loader 并不需要 css-loader 的結果,它只需要 request 后的元數據楷兽。

module: {
  rules: [{ test:/\.css$/, use: ['style-loader', 'css-loader'] }]
},

如果按正常流程走地熄,style-loader 只能拿到 css-loader 轉換的結果岩齿,一個包含可動態(tài)執(zhí)行函數的js字符串(含有模塊導出代碼勾缭,類似module.exports語句)颜懊。因此 style-loader 在 pitch 函數里返回了包含類似require(!!css-loader!./*.css)的js字符串距误。簡化如下:

const path = require('path');
const loaderUtils = require('loader-utils');
// style-loader 導出了一個空函數
module.exports = function () {
}

// style-loader 的 pitch 方法
module.exports.pitch = function(request){
  var result = [
    'var content=require(' + loaderUtils.stringifyRequest(this),'!!' + request)+')',
    'require' + loaderUtils.stringifyRequest(this,'!'+ path.join(__dirname,"add-style.js")) + ')(content)',
    'if(content.locals) module.exports = content.locals'
  ]
  return result.join(';');
}

當 webpack 執(zhí)行這個結果時慷荔,就會內聯(lián)調用require()語句中的loader進行處理莹菱。!!前綴可以禁用 webpack 配置中的所有 loader弊琴,因此不會再重復遞歸調用 style-loader翘骂,現在只會用 css-loader 處理棋弥。

除此之外核偿,傳遞給 pitch 方法的 第三個參數(data),在主函數執(zhí)行階段也會暴露在 this.data 之下顽染,可用于在循環(huán)時捕獲并共享前面的信息漾岳。比如cache-loader利用 pitch 進行緩存讀取,如果存在緩存就跳過后面 loader 的編譯粉寞。

// cache-loader 主函數
module.exports = function (...args) {
  const callback = this.async();
  const { data } = this; // 拿到 pitch 方法傳遞的 data
  const toDepDetails = (dep, mapCallback) => {
    FS.stat(dep, (err, stats) => {
      const mtime = stats.mtime.getTime();
      if (mtime / 1000 >= Math.floor(data.startTime / 1000)) {
        cache = false;
      }
    });
  };
  writeFn(data.cacheKey, {
      remainingRequest: pathWithCacheContext(options.cacheContext, data.remainingRequest),
      dependencies: deps,
      contextDependencies: contextDeps,
      result: args
    }, () => {
      // ignore errors here
      callback(null, ...args);
    });
};

// cache-loader 的 pitch 方法
module.exports.pitch = function (remainingRequest, prevRequest, dataInput) {
  const data = dataInput;
  data.remainingRequest = remainingRequest;
  data.cacheKey = cacheKeyFn(options, data.remainingRequest);
  // 根據 cacheKey 的標識獲取對應的緩存文件內容
  readFn(data.cacheKey, (readErr, cacheData) => {
    // 遍歷所有依賴文件路徑
    async.each(cacheData.dependencies.concat(cacheData.contextDependencies), (dep, eachCallback) => { 
     // ...
      FS.stat(contextDep.path, (statErr, stats) => {
        const compStats = stats;
        const compDep = contextDep;
         // 對比當前文件最新的 mtime 和緩存當中記錄的 mtime 是否一致
          if (compareFn(compStats, compDep) !== true) {
            eachCallback(true);
            return;
          }
          eachCallback();
        });
      },
      (err) => {
        if (err) {
          data.startTime = Date.now(); // 這個時間能被主函數執(zhí)行時獲取
          callback();
          return;
        }
        // ...
        callback(null, ...cacheData.result);
      }
    );
  })
};

cache-loader 的 pitch 方法會根據生成的 cacheKey 去查找 node_modules/.cache 目錄緩存的 json 文件尼荆。如果緩存文件中記錄的所有依賴以及這個文件本身都沒發(fā)生變化,那么就直接讀取緩存中的內容并返回唧垦,同時跳過后面 loader 的執(zhí)行捅儒。一旦依賴或者這個文件發(fā)生變化,那么就正常走后面 loader 的 pitch 方法振亮,以及執(zhí)行 loader 的流程巧还。

手動指定 loader 的執(zhí)行順序

用內聯(lián)方式為引入模塊指定 loader 時,在 import 語句添加前綴可以覆蓋 配置 中的所有 loader, preLoader 和 postLoader坊秸,從而影響到 pitch 和執(zhí)行的順序麸祷。
比如使用 -! 前綴,將禁用所有已配置的 preLoader 和 loader褒搔,但是不禁用 postLoaders阶牍。

// 在 /src/file.js 文件中
require('-!./loader1?xyz!loader2!./resource?rrr');
// 或
import Styles from '-!style-loader!css-loader?modules!./styles.css';

那 preLoader喷面、postLoader 是什么意思呢?
其實 prepost 都是我們用 Rule.enforce 配置項指定的loader執(zhí)行類型(可選值)荸恕,分別表示 優(yōu)先處理 和 最后處理乖酬。不指定即指按正常順序處理,即默認都是 normal loader融求。還有就是我們上面講的內聯(lián) loader咬像,也就是“行內 loader”,即 loader 被應用在 import/require 行內生宛。

Normal 階段是按 前置(pre)县昂、普通(normal)、行內(inline)陷舅、后置(post) 的順序調用loader的倒彰,Pitching 階段則相反。

如果不加 enforce 屬性莱睁,用以下順序配置loader待讳,匹配到css文件時的(單指Normal階段)默認執(zhí)行順序是 style-loader -> css-loader -> sass-loader(從下往上), 而我們按照自己的需要標上優(yōu)先和后置順序后仰剿,結果就符合預期了:sass-loader -> css-loader -> style-loader

module: {
    rules: [
      {
        test: /\.css$/, 
        loader: : 'sass-loader', 
        enforce: 'pre', // 指定為前置類型
      },
      {
        test: /\.css$/,  
        loader: : 'css-loader',  // 沒指定enforce创淡,為普通類型
      },
      {
        test: /\.css$/, 
        loader: : 'style-loader', 
        enforce: 'post', // 指定為后置類型
      }
    ]
  },

再看 webpack-chain 的實現:
根據文檔API和實測,webpack-chain 的 enforce 配置和上面的有很大的區(qū)別南吮。.enforce(preOrPost)傳的值是指先執(zhí)行上面的loader(pre) 或下面的loader (post)琳彩,不要把這里的prepost跟前置loader和后置loader混淆了。

// 速記格式解讀
config.module
  .rule(name)
    .test(test)
    .pre() // 指代 .use(loader-name-pre)
    .post() // 指代另一個 .use(loader-name-post)
    .enforce(preOrPost) // 值為'pre' 則表示先執(zhí)行上面的loader部凑,即loader-name-pre露乏;值為'post' 則先執(zhí)行下方的loader,loader-name-post

感覺這個API不是很合理??

// 本來從下到上涂邀,現在 vue-loader -> cache-loader
config.module
  .rule('vue')
    .test(/\.vue$/)
    .use('vue-loader') // 在前面表示 pre
      .loader(require.resolve('vue-loader'))
      .end()
    .use('cache-loader') // 在后面表示 post
      .loader(require.resolve('cache-loader'))
      .end()  
    .enforce('pre') // 表示先執(zhí)行 vue-loader

使用嵌套rules來跳出多余的預處理規(guī)則

可以使用屬性 rulesoneOf 來指定嵌套規(guī)則瘟仿,比較常用且是很推薦的做法。
Rule.oneOf 規(guī)則數組:只使用 oneOf 數組中第一個匹配到的規(guī)則比勉。

常規(guī)寫法比如給某類格式的文件用 module.rules 配置了3個loader猾骡,執(zhí)行時就會按 pre-> normal -> post 的順序都去處理一遍。而 oneOf 可以用 resourceQuery 屬性來查詢與資源請求字符串的查詢部分(即從?開始)相匹配的 Condition敷搪。看下例:
foo.css?inline 只會經過 url-loader 處理幢哨,而 foo.css?external 只會經過 file-loader 處理赡勘。

  module: {
    rules: [
      {
        test: /\\.css$/,
        oneOf: [
          {
            resourceQuery: /inline/, // foo.css?inline
            use: 'url-loader',
          },
          {
            resourceQuery: /external/, // foo.css?external
            use: 'file-loader',
          },
        ],
      },
    ],
  }

webpack-chain 版本:

config.module
  .rule('css')
    .oneOf('inline')
      .resourceQuery(/inline/)
      .use('url')
        .loader('url-loader')
        .end()
      .end()
    .oneOf('external')
      .resourceQuery(/external/)
      .use('file')
        .loader('file-loader')

常用loader

文件
  • url-loaderfile-loader 類似,但當文件 size 小于設置的 limit 值捞镰,會返回 data URL
  • file-loader 將文件保存至輸出文件夾中并返回 URL (默認是是絕對路徑闸与,可以 outputPath 和 publicPath 通過配置成相對路徑)
語法轉換
樣式
  • style-loader 將樣式模塊導出的內容以往 <head> 中注入多個 <style> 的形式毙替,添加到 DOM 中
  • css-loader 加載 CSS 文件并解析 @import 的 CSS 文件,將 url() 處理成 require() 請求践樱,最終返回 CSS 代碼
  • less-loader 加載并編譯 LESS 文件
  • sass-loader 加載并編譯 SASS/SCSS 文件
  • postcss-loader 使用 PostCSS 加載并轉換 CSS/SSS 文件
  • stylus-loader 加載并編譯 Stylus 文件
框架

參考:
Writing a loader
webpack使用筆記(四)loader原理與實現
手把手教你擼一個 Webpack Loader
Webpack中Loader的pitch方法

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末厂画,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子拷邢,更是在濱河造成了極大的恐慌袱院,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞭稼,死亡現場離奇詭異忽洛,居然都是意外死亡,警方通過查閱死者的電腦和手機环肘,發(fā)現死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門欲虚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人悔雹,你說我怎么就攤上這事复哆。” “怎么了腌零?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵梯找,是天一觀的道長。 經常有香客問我莱没,道長初肉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任饰躲,我火速辦了婚禮牙咏,結果婚禮上,老公的妹妹穿的比我還像新娘嘹裂。我一直安慰自己妄壶,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布寄狼。 她就那樣靜靜地躺著丁寄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪泊愧。 梳的紋絲不亂的頭發(fā)上伊磺,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音删咱,去河邊找鬼屑埋。 笑死,一個胖子當著我的面吹牛痰滋,可吹牛的內容都是我干的摘能。 我是一名探鬼主播续崖,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼团搞!你這毒婦竟也來了严望?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤逻恐,失蹤者是張志新(化名)和其女友劉穎像吻,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體梢莽,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡萧豆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了昏名。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涮雷。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖轻局,靈堂內的尸體忽然破棺而出洪鸭,到底是詐尸還是另有隱情,我是刑警寧澤仑扑,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布览爵,位于F島的核電站,受9級特大地震影響镇饮,放射性物質發(fā)生泄漏蜓竹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一储藐、第九天 我趴在偏房一處隱蔽的房頂上張望俱济。 院中可真熱鬧,春花似錦钙勃、人聲如沸蛛碌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔚携。三九已至,卻和暖如春克饶,著一層夾襖步出監(jiān)牢的瞬間酝蜒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工矾湃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留亡脑,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像远豺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子坞嘀,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容