天天看點

webpack loader從入門到精通全解析webpack loader從入門到精通

webpack loader從入門到精通

Loader

就像是一個轉換器,可以把源檔案經過轉化後輸出新的一個結果,并且一個檔案還可以鍊式的經過多個轉換器轉換。

webpack loader從入門到精通全解析webpack loader從入門到精通

以轉換處理

SCSS

檔案為例子:

SCSS

源代碼會先移交給

sass-loader

SCSS

轉換成

CSS

sass-loade

r 輸出的 CSS 交給

css-loader

處理,找出 CSS 中依賴的資源、壓縮 CSS 等;

css-loader

輸出的 CSS 交給

style-loader

處理,轉換成通過腳本加載的 JavaScript 代碼;

可以看出以上的處理過程需要有順序的鍊式執行,先

sass-loader

css-loader

style-loader

。 以上處理的 Webpack 相關配置如下:

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 處理後的結果會傳給下一個接着處理,最後的 Loader 将處理後的最終結果傳回給 Webpack。

是以,在你開發一個 Loader 時,請保持其職責的單一性,你隻需關心輸入和輸出。

Loader 基礎

由于 Webpack 是運作在 Node.js 之上的,一個 Loader 其實就是一個 Node.js 子產品,這個子產品需要導出一個函數。 這個導出的函數的工作就是獲得處理前的原内容,對原内容執行處理後,傳回處理後的内容。

一個最簡單的 Loader 的源碼如下:

module.exports = function(source) {
  // source 為 compiler 傳遞給 Loader 的一個檔案的原内容
  // 該函數需要傳回處理後的内容,這裡簡單起見,直接把原内容傳回了,相當于該 Loader 沒有做任何轉換
  return source;
};
           

由于 Loader 運作在 Node.js 中,你可以調用任何 Node.js 自帶的 API,或者安裝第三方子產品進行調用:

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

Loader 進階

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

獲得 Loader 的 options

在最上面處理 SCSS 檔案的 Webpack 配置中,給

css-loader

傳了

options

參數,以控制

css-loader

。 如何在自己編寫的 Loader 中擷取到使用者傳入的 options 呢?需要這樣做:

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,因為它們的轉換流程都是同步的,轉換完成後再傳回結果。 但在有些場景下轉換的步驟隻能是異步完成的,例如你需要通過網絡請求才能得出結果,如果采用同步的方式網絡請求就會阻塞整個建構,導緻建構非常緩慢。

webpack loader從入門到精通全解析webpack 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從入門到精通全解析webpack loader從入門到精通

如果你想讓 Webpack 不緩存該 Loader 的處理結果,可以這樣:

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

其它 Loader API

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

webpack loader從入門到精通全解析webpack loader從入門到精通
  • 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: {…})

加載本地 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 的步驟如下:

  • 確定正在開發的本地 Npm 子產品(也就是正在開發的 Loader)的

    package.json

    已經正确配置好;
  • 在本地 Npm 子產品根目錄下執行 npm link,把本地子產品注冊到全局;
  • 在項目根目錄下執行

    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。

webpack loader從入門到精通全解析webpack loader從入門到精通

該 Loader 名叫

comment-require-loader

,作用是把 JavaScript 代碼中的注釋文法

轉換成

該 Loader 的使用場景是去正确加載針對

Fis3

編寫的

JavaScript

,這些

JavaScript

中存在通過注釋的方式加載依賴的 CSS 檔案。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['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);
};
           

倉庫代碼

https://github.com/gwuhaolin/comment-require-loader