載入器是一個匯出函式的節點模組。當資源應該由這個載入器轉換時,就會呼叫這個函式。給定的函式將可以使用 載入器 API,使用提供給它的 this
內容。
在深入探討不同類型的載入器、它們的用法和範例之前,讓我們來看看在本地開發和測試載入器的三種方法。
若要測試單一載入器,可以使用 path
來 resolve
規則物件中的本地檔案
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。
當多個載入器串接時,重要的是要記住它們會以相反順序執行,也就是從右到左或從下到上,視陣列格式而定。
在以下範例中,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-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 失效,並在監控模式下重新編譯。以下是使用 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 中,會使用 @import
和 url(...)
陳述式。這些相依性應由模組系統解析。
這可以透過兩種方式之一來完成
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
/ export
和 async
/ 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 API 和 memfs
來執行 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.
它成功了!在這個時候,你應該可以開始開發、測試和部署你自己的載入器了。我們希望你與社群的其他成員分享你的創作!