撰寫載入器

載入器是一個匯出函式的節點模組。當資源應該由這個載入器轉換時,就會呼叫這個函式。給定的函式將可以使用 載入器 API,使用提供給它的 this 內容。

設定

在深入探討不同類型的載入器、它們的用法和範例之前,讓我們來看看在本地開發和測試載入器的三種方法。

若要測試單一載入器,可以使用 pathresolve 規則物件中的本地檔案

webpack.config.js

const path = require('path');

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve('path/to/loader.js'),
            options: {
              /* ... */
            },
          },
        ],
      },
    ],
  },
};

若要測試多個載入器,可以使用 resolveLoader.modules 組態來更新 webpack 搜尋載入器的位置。例如,如果專案中有本地的 /loaders 目錄

webpack.config.js

const path = require('path');

module.exports = {
  //...
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')],
  },
};

順帶一提,如果你已經為載入器建立了獨立的儲存庫和套件,你可以 npm link 到要測試的專案中。

簡單用法

當單一載入器套用至資源時,載入器會呼叫只有一個參數,也就是包含資源檔案內容的字串。

同步載入器可以 return 代表轉換模組的單一值。在更複雜的情況下,載入器可以使用 this.callback(err, values...) 函式傳回任意數量的值。錯誤會傳遞至 this.callback 函式或在同步載入器中擲回。

載入器預期會回傳一或兩個值。第一個值是結果 JavaScript 程式碼,以字串或緩衝區形式。第二個選用值是 JavaScript 物件形式的 SourceMap。

複雜用法

當多個載入器串接時,重要的是要記住它們會以相反順序執行,也就是從右到左或從下到上,視陣列格式而定。

  • 最後一個載入器會最先呼叫,會傳遞原始資源的內容。
  • 第一個載入器會最後呼叫,預期會傳回 JavaScript 和選用的原始碼對應。
  • 介於兩者之間的載入器會以串接中前一個載入器的結果執行。

在以下範例中,foo-loader 會傳遞原始資源,而 bar-loader 會接收 foo-loader 的輸出,並傳回最終轉換的模組和必要的原始碼對應。

webpack.config.js

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js/,
        use: ['bar-loader', 'foo-loader'],
      },
    ],
  },
};

準則

撰寫載入器時,應遵循以下準則。它們按重要性排序,有些只適用於特定情況,請閱讀後續的詳細部分以取得更多資訊。

  • 保持簡潔
  • 利用串接
  • 發射模組化輸出。
  • 確保它們是無狀態的。
  • 使用載入器工具程式
  • 標記載入器相依性
  • 解決模組相依性
  • 萃取共用程式碼
  • 避免絕對路徑
  • 使用同儕相依性

簡單

載入器應只執行單一任務。這不僅讓維護每個載入器的工作變得更輕鬆,還能讓它們串聯起來,以便在更多場景中使用。

串聯

善用載入器可以串聯在一起的事實。不要寫一個處理五個任務的單一載入器,而是寫五個更簡單的載入器來分攤這項工作。將它們隔離開來不僅能讓每個載入器保持簡單,還能讓它們用於你原本沒想到的事情上。

舉例來說,使用載入器選項或查詢參數指定資料來呈現範本檔案。它可以寫成一個單一載入器,從來源編譯範本、執行它,並傳回一個匯出包含 HTML 程式碼字串的模組。然而,根據準則,存在一個可以與其他開源載入器串聯的 apply-loader

  • pug-loader:將範本轉換成匯出函式的模組。
  • apply-loader:使用載入器選項執行函式,並傳回原始 HTML。
  • html-loader:接受 HTML,並輸出一個有效的 JavaScript 模組。

模組化

保持輸出模組化。Loader 生成的模組應遵循與一般模組相同的設計原則。

無狀態

確保 Loader 在模組轉換之間不保留狀態。每次執行都應獨立於其他已編譯模組,以及相同模組的先前編譯。

Loader 工具

利用 loader-utils 套件,它提供各種有用的工具。除了 loader-utils,還應使用 schema-utils 套件,以根據一致的 JSON Schema 驗證 Loader 選項。以下是同時使用兩者的簡要範例

loader.js

import { urlToRequest } from 'loader-utils';
import { validate } from 'schema-utils';

const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string',
    },
  },
};

export default function (source) {
  const options = this.getOptions();

  validate(schema, options, {
    name: 'Example Loader',
    baseDataPath: 'options',
  });

  console.log('The request path', urlToRequest(this.resourcePath));

  // Apply some transformations to the source...

  return `export default ${JSON.stringify(source)}`;
}

Loader 相依性

如果 Loader 使用外部資源(例如從檔案系統讀取),則必須指出。此資訊用於使可快取的 Loader 失效,並在監控模式下重新編譯。以下是使用 addDependency 方法達成此目的的簡要範例

loader.js

import path from 'path';

export default function (source) {
  var callback = this.async();
  var headerPath = path.resolve('header.js');

  this.addDependency(headerPath);

  fs.readFile(headerPath, 'utf-8', function (err, header) {
    if (err) return callback(err);
    callback(null, header + '\n' + source);
  });
}

模組相依性

根據模組類型,可能會使用不同的 Schema 來指定相依性。例如,在 CSS 中,會使用 @importurl(...) 陳述式。這些相依性應由模組系統解析。

這可以透過兩種方式之一來完成

  • 透過將它們轉換為 require 陳述式。
  • 使用 this.resolve 函數來解析路徑。

css-loader 是第一種方法的一個好範例。它透過將 @import 陳述式替換為對其他樣式表的 require,以及將 url(...) 替換為對所參照檔案的 require,來將相依性轉換為 require

less-loader 的情況中,它無法將每個 @import 轉換為 require,因為所有 .less 檔案都必須在一次通過中編譯,才能追蹤變數和混入。因此,less-loader 使用自訂路徑解析邏輯來擴充 less 編譯器。然後,它利用第二種方法 this.resolve,透過 webpack 來解析相依性。

共用程式碼

避免在 loader 處理的每個模組中產生共用程式碼。請改為在 loader 中建立一個執行時期檔案,並產生對那個共用模組的 require

src/loader-runtime.js

const { someOtherModule } = require('./some-other-module');

module.exports = function runtime(params) {
  const x = params.y * 2;

  return someOtherModule(params, x);
};

src/loader.js

import runtime from './loader-runtime.js';

export default function loader(source) {
  // Custom loader logic

  return `${runtime({
    source,
    y: Math.random(),
  })}`;
}

絕對路徑

不要在模組程式碼中插入絕對路徑,因為當專案的根目錄移動時,它們會中斷雜湊。您可以使用以下程式碼將絕對路徑轉換為相對路徑。

// `loaderContext` is same as `this` inside loader function
JSON.stringify(loaderContext.utils.contextify(loaderContext.context || loaderContext.rootContext, request));

同儕相依性

如果您正在使用的 loader 是另一個套件的簡單包裝器,則您應該將該套件包含為 peerDependency。這種方法允許應用程式的開發人員在 package.json 中指定確切版本(如果需要)。

例如,sass-loader 指定 node-sass 為同儕相依性,如下所示

{
  "peerDependencies": {
    "node-sass": "^4.0.0"
  }
}

測試

因此,你已經撰寫了一個載入器,遵循了上述準則,並已設定好讓它在本地執行。接下來呢?讓我們來執行一個單元測試範例,以確保我們的載入器按照我們預期的運作方式運作。我們將使用 Jest 框架來執行此操作。我們還將安裝 babel-jest 和一些預設,這些預設將允許我們使用 import / exportasync / await。讓我們從安裝並將這些儲存為 devDependencies 開始

npm install --save-dev jest babel-jest @babel/core @babel/preset-env

babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      },
    ],
  ],
};

我們的載入器將處理 .txt 檔案,並將 [name] 的任何執行個體替換為提供給載入器的 name 選項。然後,它將輸出一個有效的 JavaScript 模組,其中包含文字作為其預設匯出

src/loader.js

export default function loader(source) {
  const options = this.getOptions();

  source = source.replace(/\[name\]/g, options.name);

  return `export default ${JSON.stringify(source)}`;
}

我們將使用此載入器處理下列檔案

test/example.txt

Hey [name]!

請密切注意下一步,因為我們將使用 Node.js APImemfs 來執行 webpack。這讓我們避免將 output 發射到磁碟,並讓我們存取 stats 資料,我們可以使用這些資料來擷取轉換後的模組

npm install --save-dev webpack memfs

test/compiler.js

import path from 'path';
import webpack from 'webpack';
import { createFsFromVolume, Volume } from 'memfs';

export default (fixture, options = {}) => {
  const compiler = webpack({
    context: __dirname,
    entry: `./${fixture}`,
    output: {
      path: path.resolve(__dirname),
      filename: 'bundle.js',
    },
    module: {
      rules: [
        {
          test: /\.txt$/,
          use: {
            loader: path.resolve(__dirname, '../src/loader.js'),
            options,
          },
        },
      ],
    },
  });

  compiler.outputFileSystem = createFsFromVolume(new Volume());
  compiler.outputFileSystem.join = path.join.bind(path);

  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      if (err) reject(err);
      if (stats.hasErrors()) reject(stats.toJson().errors);

      resolve(stats);
    });
  });
};

現在,最後,我們可以撰寫我們的測試並新增一個 npm 指令碼來執行它

test/loader.test.js

/**
 * @jest-environment node
 */
import compiler from './compiler.js';

test('Inserts name and outputs JavaScript', async () => {
  const stats = await compiler('example.txt', { name: 'Alice' });
  const output = stats.toJson({ source: true }).modules[0].source;

  expect(output).toBe('export default "Hey Alice!\\n"');
});

package.json

{
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "testEnvironment": "node"
  }
}

一切就緒後,我們可以執行它並查看我們的載入器是否通過測試

 PASS  test/loader.test.js
  ✓ Inserts name and outputs JavaScript (229ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.853s, estimated 2s
Ran all test suites.

它成功了!在這個時候,你應該可以開始開發、測試和部署你自己的載入器了。我們希望你與社群的其他成員分享你的創作!

7 貢獻者

asulaimanmichael-ciniawskybyzykanikethsahajamesgeorge007chenxsandev-itsheng