深刻Webpack-編寫Loader

Loader 就像是一個翻譯員,能把源文件通過轉化後輸出新的結果,而且一個文件還能夠鏈式的通過多個翻譯員翻譯。css

以處理 SCSS 文件爲例:html

  1. SCSS 源代碼會先交給 sass-loader 把 SCSS 轉換成 CSS;
  2. 把 sass-loader 輸出的 CSS 交給 css-loader 處理,找出 CSS 中依賴的資源、壓縮 CSS 等;
  3. 把 css-loader 輸出的 CSS 交給 style-loader 處理,轉換成經過腳本加載的 JavaScript 代碼;

能夠看出以上的處理過程須要有順序的鏈式執行,先 sass-loader 再 css-loader 再 style-loader。 以上處理的 Webpack 相關配置以下:node

module.exports = {
  module: {
    rules: [
      {
        // 增長對 SCSS 文件的支持
        test: /\.scss/,
        // SCSS 文件的處理順序爲先 sass-loader 再 css-loader 再 style-loader
        use: [
          'style-loader',
          {
            loader:'css-loader',
            // 給 css-loader 傳入配置項
            options:{
              minimize:true, 
            }
          },
          'sass-loader'],
      },
    ]
  },
};
複製代碼

Loader 的職責

由上面的例子能夠看出:一個 Loader 的職責是單一的,只須要完成一種轉換。 若是一個源文件須要經歷多步轉換才能正常使用,就經過多個 Loader 去轉換。 在調用多個 Loader 去轉換一個文件時,每一個 Loader 會鏈式的順序執行, 第一個 Loader 將會拿到需處理的原內容,上一個 Loader 處理後的結果會傳給下一個接着處理,最後的 Loader 將處理後的最終結果返回給 Webpack。webpack

因此,在你開發一個 Loader 時,請保持其職責的單一性,你只需關心輸入和輸出。git

Loader 基礎

因爲 Webpack 是運行在 Node.js 之上的,一個 Loader 其實就是一個 Node.js 模塊,這個模塊須要導出一個函數。 這個導出的函數的工做就是得到處理前的原內容,對原內容執行處理後,返回處理後的內容。github

一個最簡單的 Loader 的源碼以下:web

module.exports = function(source) {
  // source 爲 compiler 傳遞給 Loader 的一個文件的原內容
  // 該函數須要返回處理後的內容,這裏簡單起見,直接把原內容返回了,至關於該 Loader 沒有作任何轉換
  return source;
};
複製代碼

因爲 Loader 運行在 Node.js 中,你能夠調用任何 Node.js 自帶的 API,或者安裝第三方模塊進行調用:npm

const sass = require('node-sass');
module.exports = function(source) {
  return sass(source);
};
複製代碼

Loader 進階

以上只是個最簡單的 Loader,Webpack 還提供一些 API 供 Loader 調用,下面來一一介紹。json

得到 Loader 的 options

在最上面處理 SCSS 文件的 Webpack 配置中,給 css-loader 傳了 options 參數,以控制 css-loader。 如何在本身編寫的 Loader 中獲取到用戶傳入的 options 呢?須要這樣作:api

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 獲取到用戶給當前 Loader 傳入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};
複製代碼

返回其它結果

上面的 Loader 都只是返回了原內容轉換後的內容,但有些場景下還須要返回除了內容以外的東西。

例如以用 babel-loader 轉換 ES6 代碼爲例,它還須要輸出轉換後的 ES5 代碼對應的 Source Map,以方便調試源碼。 爲了把 Source Map 也一塊兒隨着 ES5 代碼返回給 Webpack,能夠這樣寫:

module.exports = function(source) {
  // 經過 this.callback 告訴 Webpack 返回的結果
  this.callback(null, source, sourceMaps);
  // 當你使用 this.callback 返回內容時,該 Loader 必須返回 undefined,
  // 以讓 Webpack 知道該 Loader 返回的結果在 this.callback 中,而不是 return 中 
  return;
};
複製代碼

其中的 this.callback 是 Webpack 給 Loader 注入的 API,以方便 Loader 和 Webpack 之間通訊。 this.callback 的詳細使用方法以下:

this.callback(
    // 當沒法轉換原內容時,給 Webpack 返回一個 Error
    err: Error | null,
    // 原內容轉換後的內容
    content: string | Buffer,
    // 用於把轉換後的內容得出原內容的 Source Map,方便調試
    sourceMap?: SourceMap,
    // 若是本次轉換爲原內容生成了 AST 語法樹,能夠把這個 AST 返回,
    // 以方便以後須要 AST 的 Loader 複用該 AST,以免重複生成 AST,提高性能
    abstractSyntaxTree?: AST
);
複製代碼

Source Map 的生成很耗時,一般在開發環境下才會生成 Source Map,其它環境下不用生成,以加速構建。 爲此 Webpack 爲 Loader 提供了 this.sourceMap API 去告訴 Loader 當前構建環境下用戶是否須要 Source Map。 若是你編寫的 Loader 會生成 Source Map,請考慮到這點。

同步與異步

Loader 有同步和異步之分,上面介紹的 Loader 都是同步的 Loader,由於它們的轉換流程都是同步的,轉換完成後再返回結果。 但在有些場景下轉換的步驟只能是異步完成的,例如你須要經過網絡請求才能得出結果,若是採用同步的方式網絡請求就會阻塞整個構建,致使構建很是緩慢。

在轉換步驟是異步時,你能夠這樣:

module.exports = function(source) {
    // 告訴 Webpack 本次轉換是異步的,Loader 會在 callback 中回調結果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 經過 callback 返回異步執行後的結果
        callback(err, result, sourceMaps, ast);
    });
};
複製代碼

處理二進制數據

在默認的狀況下,Webpack 傳給 Loader 的原內容都是 UTF-8 格式編碼的字符串。 但有些場景下 Loader 不是處理文本文件,而是處理二進制文件,例如 file-loader,就須要 Webpack 給 Loader 傳入二進制格式的數據。 爲此,你須要這樣編寫 Loader:

module.exports = function(source) {
    // 在 exports.raw === true 時,Webpack 傳給 Loader 的 source 是 Buffer 類型的
    source instanceof Buffer === true;
    // Loader 返回的類型也能夠是 Buffer 類型的
    // 在 exports.raw !== true 時,Loader 也能夠返回 Buffer 類型的結果
    return source;
};
// 經過 exports.raw 屬性告訴 Webpack 該 Loader 是否須要二進制數據 
module.exports.raw = true;
複製代碼

以上代碼中最關鍵的代碼是最後一行 module.exports.raw = true;,沒有該行 Loader 只能拿到字符串。

緩存加速

在有些狀況下,有些轉換操做須要大量計算很是耗時,若是每次構建都從新執行重複的轉換操做,構建將會變得很是緩慢。 爲此,Webpack 會默認緩存全部 Loader 的處理結果,也就是說在須要被處理的文件或者其依賴的文件沒有發生變化時, 是不會從新調用對應的 Loader 去執行轉換操做的。

若是你想讓 Webpack 不緩存該 Loader 的處理結果,能夠這樣:

module.exports = function(source) {
  // 關閉該 Loader 的緩存功能
  this.cacheable(false);
  return source;
};
複製代碼

其它 Loader API

除了以上提到的在 Loader 中能調用的 Webpack API 外,還存在如下經常使用 API:

  • this.context:當前處理文件的所在目錄,假如當前 Loader 處理的文件是 /src/main.js,則 this.context 就等於 /src

  • this.resource:當前處理文件的完整請求路徑,包括 querystring,例如 /src/main.js?name=1

  • this.resourcePath:當前處理文件的路徑,例如 /src/main.js

  • this.resourceQuery:當前處理文件的 querystring。

  • this.target:等於 Webpack 配置中的 Target,詳情見 2-7其它配置項-Target

  • this.loadModule:但 Loader 在處理一個文件時,若是依賴其它文件的處理結果才能得出當前文件的結果時, 就能夠經過 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去得到 request 對應文件的處理結果。

  • this.resolve:像 require 語句同樣得到指定文件的完整路徑,使用方法爲 resolve(context: string, request: string, callback: function(err, result: string))

  • this.addDependency:給當前處理文件添加其依賴的文件,以便再其依賴的文件發生變化時,會從新調用 Loader 處理該文件。使用方法爲 addDependency(file: string)

  • this.addContextDependency:和 addDependency 相似,但 addContextDependency 是把整個目錄加入到當前正在處理文件的依賴中。使用方法爲 addContextDependency(directory: string)

  • this.clearDependencies:清除當前正在處理文件的全部依賴,使用方法爲 clearDependencies()

  • this.emitFile:輸出一個文件,使用方法爲 emitFile(name: string, content: Buffer|string, sourceMap: {...})

其它沒有提到的 API 能夠去 Webpack 官網 查看。

加載本地 Loader

在開發 Loader 的過程當中,爲了測試編寫的 Loader 是否能正常工做,須要把它配置到 Webpack 中後,纔可能會調用該 Loader。 在前面的章節中,使用的 Loader 都是經過 Npm 安裝的,要使用 Loader 時會直接使用 Loader 的名稱,代碼以下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css/,
        use: ['style-loader'],
      },
    ]
  },
};
複製代碼

若是還採起以上的方法去使用本地開發的 Loader 將會很麻煩,由於你須要確保編寫的 Loader 的源碼是在 node_modules 目錄下。 爲此你須要先把編寫的 Loader 發佈到 Npm 倉庫後再安裝到本地項目使用。

解決以上問題的便捷方法有兩種,分別以下:

Npm link

Npm link 專門用於開發和調試本地 Npm 模塊,能作到在不發佈模塊的狀況下,把本地的一個正在開發的模塊的源碼連接到項目的 node_modules 目錄下,讓項目能夠直接使用本地的 Npm 模塊。 因爲是經過軟連接的方式實現的,編輯了本地的 Npm 模塊代碼,在項目中也能使用到編輯後的代碼。

完成 Npm link 的步驟以下:

  1. 確保正在開發的本地 Npm 模塊(也就是正在開發的 Loader)的 package.json 已經正確配置好;
  2. 在本地 Npm 模塊根目錄下執行 npm link,把本地模塊註冊到全局;
  3. 在項目根目錄下執行 npm link loader-name,把第2步註冊到全局的本地 Npm 模塊連接到項目的 node_moduels 下,其中的 loader-name 是指在第1步中的 package.json 文件中配置的模塊名稱。

連接好 Loader 到項目後你就能夠像使用一個真正的 Npm 模塊同樣使用本地的 Loader 了。

ResolveLoader

2-7其它配置項 中曾介紹過 ResolveLoader 用於配置 Webpack 如何尋找 Loader。 默認狀況下只會去 node_modules 目錄下尋找,爲了讓 Webpack 加載放在本地項目中的 Loader 須要修改 resolveLoader.modules

假如本地的 Loader 在項目目錄中的 ./loaders/loader-name 中,則須要以下配置:

module.exports = {
  resolveLoader:{
    // 去哪些目錄下尋找 Loader,有前後順序之分
    modules: ['node_modules','./loaders/'],
  }
}
複製代碼

加上以上配置後, Webpack 會先去 node_modules 項目下尋找 Loader,若是找不到,會再去 ./loaders/ 目錄下尋找。

實戰

上面講了許多理論,接下來從實際出發,來編寫一個解決實際問題的 Loader。

該 Loader 名叫 comment-require-loader,做用是把 JavaScript 代碼中的註釋語法

// @require '../style/index.css'
複製代碼

轉換成

require('../style/index.css');
複製代碼

該 Loader 的使用場景是去正確加載針對 Fis3 編寫的 JavaScript,這些 JavaScript 中存在經過註釋的方式加載依賴的 CSS 文件。

該 Loader 的使用方法以下:

module.exports = {
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ['comment-require-loader'],
        // 針對採用了 fis3 CSS 導入語法的 JavaScript 文件經過 comment-require-loader 去轉換 
        include: [path.resolve(__dirname, 'node_modules/imui')]
      }
    ]
  }
};
複製代碼

該 Loader 的實現很是簡單,完整代碼以下:

function replace(source) {
    // 使用正則把 // @require '../style/index.css' 轉換成 require('../style/index.css'); 
    return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);');
}

module.exports = function (content) {
    return replace(content);
};
複製代碼

本實例提供項目完整代碼

《深刻淺出Webpack》全書在線閱讀連接

閱讀原文

相關文章
相關標籤/搜索