基本概念

  • Loader​:對單個資源(文件)做轉換的函數(把一個文件從一種內容轉換為另一種內容),在 module 層面運行。
  • Plugin​:在整個構建過程的生命週期裏插入鈎子邏輯(修改編譯器、生成資源、注入行為等),在 compiler/compilation 層面運行。

概念説明

  • Loader
    • 輸入:某個源文件(例如 .js.css.png)的內容(通常是字符串或 Buffer),也可以是上一個 loader 產生的結果文件。
    • 輸出:處理後的模塊內容(通常是 JS 源碼字符串、source map、或者返回多個值)。
    • 用途舉例:Babel 轉碼(babel-loader)、把 CSS 轉成 JS 模塊(css-loader + style-loadermini-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.emitcompilation.hooks.seal 等。

loader 的工作原理

  1. 調用方式與順序
    1. 當一個文件被匹配到 module.rules.use(或 loader)時,webpack 會把這個文件交給 loader 鏈處理。鏈按從右到左從下到上的順序執行:最後一個 loader 最先執行(先對原始資源做最底層轉換),第一個 loader 最後執行,最終輸出 JS 模塊。
    2. 例如 use: ['style-loader','css-loader','postcss-loader']:先執行 postcss-loader(處理 CSS),然後 css-loader(把 CSS 轉成 JS 導出),最後 style-loader(把樣式注入到 DOM)。
  2. 同步 vs 異步
    1. loader 默認是同步的(同步返回可以直接使用 return 或者 this.callback(null, recource));若需異步處理(例如異步讀取文件、運行外部工具),要在 loader 裏調用 const callback = this.async(),隨後異步完成後 callback(null, result, sourceMap)
  3. LoaderContext(this)
    1. 在 loader 中 this 指向 LoaderContext,含有諸多有用屬性/方法:this.async()this.cacheable()this.resourcePaththis.getOptions()this.emitWarning()this.emitError()this.addDependency() 等。比如 this.cacheable() 用來告訴 webpack 此 loader 的結果可被緩存(現在很多 bake-in 已默認 cache)。
  4. Pitching Loader
    1. loader 分為 PitchingLoader 和 NormalLoader,他們的執行順序是相反的。PitchingLoader 的優先級更高。如果希望改變執行順序,可以通過 enforce 配置(pre, normal, inline, post)。
    2. loader 可以定義 pitch 方法,webpack 會先按順序先調用每個 loader 的 pitch(從左到右),若某個 pitch 返回非 undefined,則後續 loader 的 normal phase 不會執行,直接進入回退階段。pitch 用於攔截並改變執行流程。
  5. source map 支持
    1. loader 在輸出源碼時應儘量返回 source map(第三個參數)來保持調試體驗。

plugin 的工作原理

  1. Tapable 與生命週期
    1. webpack 的 plugin 基於 Tapable 實現,通過 compiler.hookscompilation.hooks 提供大量鈎子(compiler 鈎子 | webpack 中文文檔),這些鈎子大致可以分為同步和異步兩大類。插件通過 compiler.hooks.someHook.tap(或 tapAsync, tapPromise)來註冊回調。
    2. 典型鈎子:beforeRun, run, compile, thisCompilation, compilation, emit, afterEmit, done 等。要根據插件目標選擇合適的鈎子。
  2. Compiler vs Compilation
    1. compiler​:表示 webpack 的整個配置 / 生命週期(針對一次完整的 webpack 實例)。
    2. compilation​:表示一次構建(compile)過程,包含模塊解析、生成 chunk、生成 assets 等。多個 compilation 可能由同一 compiler 觸發(例如 watch 模式中的每次增量構建)。
  3. Hook 的使用過程:
    1. 創建 Hook 對象
    2. 註冊 Hook 中的事件
    3. 觸發事件
  4. Hook 被註冊到 Webpack 生命週期的過程:
    1. 在 Webpack 的 createCompiler 方法中,註冊了所有的插件
    2. 註冊插件時,會調用插件函數或者插件對象的 apply 方法
    3. 插件對象會接收 compiler 對象,可以通過 compiler 對象來註冊 Hook 的事件
    4. 某些插件也會傳入 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))
  };
});