Loader(載入器) 是 webpack 的核心之一。它用於將不同類型的文件轉換為 webpack 可識別的模塊。本文將深入探索 webpack 中的 loader,揭秘它的工作原理,以及如何開發一個 loader。 ...
前言
Loader(載入器) 是 webpack 的核心之一。它用於將不同類型的文件轉換為 webpack 可識別的模塊。本文將嘗試深入探索 webpack 中的 loader,揭秘它的工作原理,以及如何開發一個 loader。
一、Loader 工作原理
webpack 只能直接處理 javascript 格式的代碼。任何非 js 文件都必須被預先處理轉換為 js 代碼,才可以參與打包。loader(載入器)就是這樣一個代碼轉換器。它由 webpack 的 loader runner
執行調用,接收原始資源數據作為參數(當多個載入器聯合使用時,上一個loader的結果會傳入下一個loader),最終輸出 javascript 代碼(和可選的 source map)給 webpack 做進一步編譯。
二、 Loader 執行順序
1. 分類
- pre: 前置loader
- normal: 普通loader
- inline: 內聯loader
- post: 後置loader
2. 執行優先順序
- 4類 loader 的執行優級為:
pre > normal > inline > post
。 - 相同優先順序的 loader 執行順序為:
從右到左,從下到上
。
3. 首碼的作用
內聯 loader 可以通過添加不同首碼,跳過其他類型 loader。
!
跳過 normal loader。-!
跳過 pre 和 normal loader。!!
跳過 pre、 normal 和 post loader。
這些首碼在很多場景下非常有用。
三、如何開發一個loader
loader 是一個導出一個函數的 node 模塊。
1. 最簡單的 loader
當只有一個 loader 應用於資源文件時,它接收源碼作為參數,輸出轉換後的 js 代碼。
// loaders/simple-loader.js
module.exports = function loader (source) {
console.log('simple-loader is working');
return source;
}
這就是一個最簡單的 loader 了,這個 loader 啥也沒乾,就是接收源碼,然後原樣返回,為了證明這個loader被調用了,我在裡面列印了一句話‘simple-loader is working’。
測試這個 loader:
需要先配置 loader 路徑
若是使用 npm 安裝的第三方 loader,直接寫 loader 的名字就可以了。但是現在用的是自己開發的本地 loader,需要我們手動配置路徑,告訴 webpack 這些 loader 在哪裡。
// webpack.config.js
const path = require('path');
module.exports = {
entry: {...},
output: {...},
module: {
rules: [
{
test: /\.js$/,
// 直接指明 loader 的絕對路徑
use: path.resolve(__dirname, 'loaders/simple-loader')
}
]
}
}
如果覺得這樣配置本地 loader 並不優雅,可以在 webpack配置本地loader的四種方法 中挑一個你喜歡的。
執行webpack編譯
可以看到,控制台輸出 ‘simple-loader is working’。說明 loader 成功被調用。
2. 帶 pitch 的 loader
pitch
是 loader 上的一個方法,它的作用是阻斷 loader 鏈。
// loaders/simple-loader-with-pitch.js
module.exports = function (source) {
console.log('normal excution');
return source;
}
// loader上的pitch方法,非必須
module.exports.pitch = function() {
console.log('pitching graph');
// todo
}
pitch 方法不是必須的。如果有 pitch,loader 的執行則會分為兩個階段:pitch
階段 和 normal execution
階段。webpack 會先從左到右執行 loader 鏈中的每個 loader 上的 pitch 方法(如果有),然後再從右到左執行 loader 鏈中的每個 loader 上的普通 loader 方法。
假如配置瞭如下 loader 鏈:
use: ['loader1', 'loader2', 'loader3']
真實的 loader 執行過程是:
在這個過程中如果任何 pitch 有返回值,則 loader 鏈被阻斷。webpack 會跳過後面所有的的 pitch 和 loader,直接進入上一個 loader 的 normal execution
。
假設在 loader2 的 pitch 中返回了一個字元串,此時 loader 鏈發生阻斷:
3. 寫一個簡版的 style-loader
style-loader 通常不會獨自使用,而是跟 css-loader 連用。css-loader 的返回值是一個 js 模塊,大致長這樣:
// 列印 css-loader 的返回值
// Imports
var ___CSS_LOADER_API_IMPORT___ = require("../node_modules/css-loader/dist/runtime/api.js");
exports = ___CSS_LOADER_API_IMPORT___(false);
// Module
exports.push([module.id, "\nbody {\n background: yellow;\n}\n", ""]);
// Exports
module.exports = exports;
這個模塊在運行時上下文中執行後返回 css
代碼 "\nbody {\n background: yellow;\n}\n"
。
style-loader 的作用就是將這段 css
代碼轉成 style
標簽插入到 html
的 head
中。
設計思路
- style-loader 最終需返回一個
js
腳本:在腳本中創建一個style
標簽,將css
代碼賦給style
標簽,再將這個style
標簽插入html
的head
中。 - 難點是獲取
css
代碼,因為 css-loader 的返回值只能在運行時的上下文中執行,而執行 loader 是在編譯階段。換句話說,css-loader 的返回值在 style-loader 里派不上用場。 - 曲線救國方案:使用獲取
css
代碼的表達式,在運行時再獲取 css (類似require('css-loader!index.css')
)。 - 在處理 css 的 loader 中又去調用
inline loader
requirecss
文件,會產生迴圈執行 loader 的問題,所以我們需要利用pitch
方法,讓 style-loader 在pitch
階段返回腳本,跳過剩下的 loader,同時還需要內聯首碼!!
的加持。
註:pitch 方法有3個參數:
- remainingRequest:loader鏈中排在自己後面的 loader 以及資源文件的絕對路徑以
!
作為連接符組成的字元串。 - precedingRequest:loader鏈中排在自己前面的 loader 的絕對路徑以
!
作為連接符組成的字元串。 - data:每個 loader 中存放在上下文中的固定欄位,可用於 pitch 給 loader 傳遞數據。
可以利用
remainingRequest
參數獲取 loader 鏈的剩餘部分。
實現
// loaders/simple-style-loader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// do nothing
}
module.exports.pitch = function(remainingRequest) {
console.log('simple-style-loader is working');
// 在 pitch 階段返回腳本
return (
`
// 創建 style 標簽
let style = document.createElement('style');
/**
* 利用 remainingRequest 參數獲取 loader 鏈的剩餘部分
* 利用 ‘!!’ 首碼跳過其他 loader
* 利用 loaderUtils 的 stringifyRequest 方法將模塊的絕對路徑轉為相對路徑
* 將獲取 css 的 require 表達式賦給 style 標簽
*/
style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)});
// 將 style 標簽插入 head
document.head.appendChild(style);
`
)
}
一個簡易的 style-loader 就完成了。
試用
webpack 配置
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {...},
output: {...},
// 手動配置 loader 路徑
resolveLoader: {
modules: [path.resolve(__dirname, 'loaders'), 'node_modules']
},
module: {
rules: [
{
// 配置處理 css 的 loader
test: /\.css$/,
use: ['simple-style-loader', 'css-loader']
}
]
},
plugins: [
// 渲染首頁
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
在 index.js 中引入一個 css 樣式文件
// src/index.js
require('./index.css');
console.log('Brovo!');
樣式文件中將 body 的背景色設置為黃色
// src/index.css
body {
background-color: yellow;
}
執行webpack
npm run build
可以看到命令行控制台列印了 'simple-style-loader is working',說明 webpack 成功調用了我們編寫的 loader。
在瀏覽器打開 dist 下的 index.html 頁面,可以看到樣式生效,而且成功插入到了頁面頭部!
說明我們編寫的 loader 發揮作用了。
成功!
三、一些 tips
推薦2個工具包
開發 loader 必備:
1. loader-utils
這個模塊中常用的幾個方法:
- getOptions 獲取 loader 的配置項。
- interpolateName 處理生成文件的名字。
- stringifyRequest 把絕對路徑處理成相對根目錄的相對路徑。
2. schema-utils
這個模塊可以幫你驗證 loader option 配置的合法性。
用法:
// loaders/simple-loader-with-validate.js
const loaderUtils = require('loader-utils');
const validate = require('schema-utils');
module.exports = function(source) {
// 獲取 loader 配置項
let options = loaderUtils.getOptions(this) || {};
// 定義配置項結構和類型
let schema = {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
// 驗證配置項是否符合要求
validate(schema, options);
return source;
}
當配置項不符合要求,編譯就會中斷併在控制台列印錯誤信息:
開發非同步 loader
非同步 loader 的開發(例如裡面有一些需要讀取文件的操作的時候),需要通過 this.async() 獲取非同步回調,然後手動調用它。
用法:
// loaders/simple-async-loader.js
module.exports = function(source) {
console.log('async loader');
let cb = this.async();
setTimeout(() => {
console.log('ok');
// 在非同步回調中手動調用 cb 返回處理結果
cb(null, source);
}, 3000);
}
註: 非同步回調 cb() 的第一個參數是
error
,要返回的結果放在第二個參數。
raw loader
如果是處理圖片、字體等資源的 loader,需要將 loader 上的 raw 屬性設置為 true,讓 loader 支持二進位格式資源(webpack預設是以 utf-8
的格式讀取文件內容給 loader)。
用法:
// loaders/simple-raw-loader.js
module.exports = function(source) {
// 將輸出 buffer 類型的二進位數據
console.log(source);
// todo handle source
let result = 'results of processing source'
return `
module.exports = '${result}'
`;
}
// 告訴 wepack 這個 loader 需要接收的是二進位格式的數據
module.exports.raw = true;
註:通常 raw 屬性會在有文件輸出需求的 loader 中使用。
輸出文件
在開發一些處理資源文件(比如圖片、字體等)的 loader 中,需要拷貝或者生成新的文件,可以使用內部的 this.emitFile()
方法.
用法:
// loaders/simple-file-loader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// 獲取 loader 的配置項
let options = loaderUtils.getOptions(this) || {};
// 獲取用戶設置的文件名或者製作新的文件名
// 註意第三個參數,是計算 contenthash 的依據
let url = loaderUtils.interpolateName(this, options.filename || '[contenthash].[ext]', {content: source});
// 輸出文件
this.emitFile(url, source);
// 返回導出文件地址的模塊腳本
return `module.exports = '${JSON.stringify(url)}'`;
}
module.exports.raw = true;
在這個例子中,loader 讀取圖片內容(buffer),將其重命名,然後調用
this.emitFile()
輸出到指定目錄,最後返回一個模塊,這個模塊導出重命名後的圖片地址。於是當require
圖片的時候,就相當於 require 了一個模塊,從而得到最終的圖片路徑。(這就是 file-loader 的基本原理)
開發約定
為了讓我們的 loader 具有更高的質量和復用性,記得保持簡單。也就是儘量保持讓一個 loader 專註一件事情,如果發現你寫的 loader 比較龐大,可以試著將其拆成幾個 loader 。
在 webpack 社區,有一份 loader 開發準則,我們可以去參考它來指導我們的 loader 設計:
- 保持簡單。
- 利用多個loader鏈。
- 模塊化輸出。
- 確保loader是無狀態的。
- 使用 loader-utils 包。
- 標記載入程式依賴項。
- 解析模塊依賴關係。
- 提取公共代碼。
- 避免絕對路徑。
- 使用 peerDependency 對等依賴項。
四、總結
loader 的本質是一個 node 模塊,這個模塊導出一個函數,這個函數上可能還有一個 pitch 方法。
瞭解了 loader 的本質和 loader 鏈的執行機制,其實就已經具備了 loader 開發基礎了。
開發 loader 不難上手,但是要開發一款高質量的 loader,仍需不斷實踐。
嘗試自己開發維護一個小 loader 吧~ 沒準以後可以通過自己編寫 loader 來解決項目中的一些實際問題。
文章源碼獲取:https://github.com/yc111/webpack-loader
歡迎交流~
Happy New Year!
--
參考
https://webpack.js.org/concepts/#loaders
https://webpack.js.org/api/loaders/
https://webpack.js.org/contribute/writing-a-loader/
https://github.com/webpack/webpack/blob/v4.41.5/lib/NormalModuleFactory.js
https://github.com/webpack-contrib/style-loader/blob/master/src/index.js
https://www.npmjs.com/package/loader-utils
https://www.npmjs.com/package/schema-utils
歡迎轉載,轉載請註明出處:
https://champyin.com/2020/01/28/%E6%8F%AD%E7%A7%98webpack-loader/