基本概念
- Loader:對單個資源(文件)做轉換的函數(把一個文件從一種內容轉換為另一種內容),在 module 層面運行。
- Plugin:在整個構建過程的生命週期裏插入鈎子邏輯(修改編譯器、生成資源、注入行為等),在 compiler/compilation 層面運行。
概念説明
- Loader
- 輸入:某個源文件(例如
.js、.css、.png)的內容(通常是字符串或 Buffer),也可以是上一個 loader 產生的結果文件。 - 輸出:處理後的模塊內容(通常是 JS 源碼字符串、source map、或者返回多個值)。
- 用途舉例:Babel 轉碼(
babel-loader)、把 CSS 轉成 JS 模塊(css-loader+style-loader或mini-css-extract-plugin)或把圖片轉成 base64(以前的url-loader)等。 - 運行時機:模塊解析/構建時,按
module.rules中的匹配依序(從右到左/從下到上)調用 loader 流水線(內部由 loader-runner 這個庫調用這些 loader 函數)。
- 輸入:某個源文件(例如
- Plugin
- 輸入/輸出:不是對單文件變換,而是對構建過程(compiler、compilation)進行操作。
- 用途舉例:生成 HTML(
html-webpack-plugin)、抽取 CSS(mini-css-extract-plugin)、定義全局常量(DefinePlugin)、並行類型檢查(ForkTsCheckerWebpackPlugin)、熱更新(HotModuleReplacementPlugin)等。 - 運行時機:在 webpack 的生命週期鈎子上(底層使用 Tapable 這個庫中的 Hooks 管理生命週期的階段執行順序)被調用,比如
compiler.hooks.emit、compilation.hooks.seal等。
loader 的工作原理
- 調用方式與順序
- 當一個文件被匹配到
module.rules.use(或loader)時,webpack 會把這個文件交給 loader 鏈處理。鏈按從右到左或從下到上的順序執行:最後一個 loader 最先執行(先對原始資源做最底層轉換),第一個 loader 最後執行,最終輸出 JS 模塊。 - 例如
use: ['style-loader','css-loader','postcss-loader']:先執行postcss-loader(處理 CSS),然後css-loader(把 CSS 轉成 JS 導出),最後style-loader(把樣式注入到 DOM)。
- 當一個文件被匹配到
- 同步 vs 異步
- loader 默認是同步的(同步返回可以直接使用
return或者this.callback(null, recource));若需異步處理(例如異步讀取文件、運行外部工具),要在 loader 裏調用const callback = this.async(),隨後異步完成後callback(null, result, sourceMap)。
- loader 默認是同步的(同步返回可以直接使用
- LoaderContext(this)
- 在 loader 中
this指向 LoaderContext,含有諸多有用屬性/方法:this.async()、this.cacheable()、this.resourcePath、this.getOptions()、this.emitWarning()、this.emitError()、this.addDependency()等。比如this.cacheable()用來告訴 webpack 此 loader 的結果可被緩存(現在很多 bake-in 已默認 cache)。
- 在 loader 中
- Pitching Loader
- loader 分為 PitchingLoader 和 NormalLoader,他們的執行順序是相反的。PitchingLoader 的優先級更高。如果希望改變執行順序,可以通過 enforce 配置(pre, normal, inline, post)。
- loader 可以定義
pitch方法,webpack 會先按順序先調用每個 loader 的 pitch(從左到右),若某個 pitch 返回非 undefined,則後續 loader 的 normal phase 不會執行,直接進入回退階段。pitch 用於攔截並改變執行流程。
- source map 支持
- loader 在輸出源碼時應儘量返回 source map(第三個參數)來保持調試體驗。
plugin 的工作原理
- Tapable 與生命週期
- webpack 的 plugin 基於 Tapable 實現,通過
compiler.hooks和compilation.hooks提供大量鈎子(compiler 鈎子 | webpack 中文文檔),這些鈎子大致可以分為同步和異步兩大類。插件通過compiler.hooks.someHook.tap(或tapAsync,tapPromise)來註冊回調。 - 典型鈎子:
beforeRun,run,compile,thisCompilation,compilation,emit,afterEmit,done等。要根據插件目標選擇合適的鈎子。
- webpack 的 plugin 基於 Tapable 實現,通過
- Compiler vs Compilation
- compiler:表示 webpack 的整個配置 / 生命週期(針對一次完整的 webpack 實例)。
- compilation:表示一次構建(compile)過程,包含模塊解析、生成 chunk、生成 assets 等。多個 compilation 可能由同一 compiler 觸發(例如 watch 模式中的每次增量構建)。
- Hook 的使用過程:
- 創建 Hook 對象
- 註冊 Hook 中的事件
- 觸發事件
- Hook 被註冊到 Webpack 生命週期的過程:
- 在 Webpack 的 createCompiler 方法中,註冊了所有的插件
- 註冊插件時,會調用插件函數或者插件對象的 apply 方法
- 插件對象會接收 compiler 對象,可以通過 compiler 對象來註冊 Hook 的事件
- 某些插件也會傳入 compilation 對象,也可以監聽 compilation 的 Hook 事件
常見 loader
babel-loader:JS/TS 轉譯(配合@babel/core)。ts-loader/esbuild-loader/swc-loader:TypeScript 或替代編譯器。css-loader:把 CSS 轉成 JS imports(解析@import/url())。style-loader:把 CSS 注入 DOM(開發用)。mini-css-extract-plugin.loader:用於生產環境把 CSS 抽取成單文件。postcss-loader:運行 PostCSS(autoprefixer、cssnano 等)。sass-loader/less-loader:預處理器。file-loader/url-loader(Webpack5 可用 asset modules 替代,已內置):處理圖片/字體等資源。html-loader:把 HTML 文件作為模塊處理(常配合 html-webpack-plugin)。thread-loader:把後面的 loader 放到 worker 線程中執行(加速 heavy loader)。
常見 plugin
HtmlWebpackPlugin:生成 HTML 文件並注入 script/css。DefinePlugin:在編譯時定義全局常量(例如process.env.NODE_ENV)。MiniCssExtractPlugin:將 CSS 從 JS 中抽離到單獨文件(生產用)。HotModuleReplacementPlugin:啓用 HMR。CleanWebpackPlugin:構建前清理輸出目錄。CopyWebpackPlugin:複製靜態資源到輸出目錄。ForkTsCheckerWebpackPlugin:在單獨進程做 TypeScript 類型檢查(配合 ts-loader)。TerserPlugin:壓縮 JS(通常在 optimization.minimizer 中)CssMinimizerPlugin:壓縮 CSS。BundleAnalyzerPlugin(webpack-bundle-analyzer):分析打包產物體積。ProvidePlugin:自動注入模塊(例如$映射到 jQuery)。
編寫一個簡單 loader(示例)
同步 loader(最基礎):
// my-loader.js
// source 資源文件內容;map sourcemap 相關數據;meta 一些元數據
module.exports = function(source, map, meta) {
// this is loader context
const options = this.getOptions?.() || {};
// 對 source 做變換
const result = source.replace(/FROM_LOWER/g, 'from lower');
// 返回處理後的 source
return result;
}
異步 loader(使用 this.async):
module.exports = function(source) {
const callback = this.async();
someAsyncTransform(source, (err, result) => {
if (err) return callback(err);
callback(null, result);
});
}
Pitch loader:
module.exports.pitch = function(remainingRequest) {
// 當某些條件滿足時返回結果,阻止後續 normal 階段執行
if (someCondition) {
return `module.exports = require(${JSON.stringify('!' + remainingRequest)})`;
}
}
編寫一個簡單 plugin(示例)
最小插件骨架:
class MyPlugin {
constructor(options) { this.options = options; }
// 注意 apply 只是掛鈎的入口點,所有工作都是在特定鈎子裏做的。
apply(compiler) {
// emit鈎子:輸出 asset 到 output 目錄之前執行。
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// 在 compilation.assets 上添加或修改資源
compilation.assets['banner.txt'] = {
source: () => 'hello world',
size: () => 11
};
callback();
});
}
}
module.exports = MyPlugin;
用 Promise / tapPromise:
compiler.hooks.emit.tapPromise('MyPlugin', async (compilation) => {
const data = await fetchSomething();
compilation.assets['data.json'] = {
source: () => JSON.stringify(data),
size: () => Buffer.byteLength(JSON.stringify(data))
};
});