webpack 插件機制分析及開發調試
- 2019 年 12 月 20 日
- 筆記

◆◆
webpack插件機制
◆◆
webpack 插件機制是整個 webpack 工具的骨架,而 webpack 本身也是利用這套插件機制構建出來的。
插件概念
專註處理 webpack 在編譯過程中的某個特定的任務的功能模組,可以稱為插件。最常見的如 html-webpack-plugin 。
那麼怎麼樣的一個東西可以稱之為 webpack 插件呢?一個完整的 webpack 插件需要滿足以下幾點規則和特徵:
- 是一個獨立的模組。
- 模組對外暴露一個 js 函數。
- 函數的原型 (prototype) 上定義了一個注入 compiler 對象的 apply 方法。
- apply 函數中需要有通過 compiler 對象掛載的 webpack 事件鉤子,鉤子的回調中能拿到當前編譯的 compilation 對象,如果是非同步編譯插件的話可以拿到回調 callback。
- 完成自定義子編譯流程並處理 complition 對象的內部數據。
- 如果非同步編譯插件的話,數據處理完成後執行 callback 回調。
Webpakck插件的基本模型
// 1、BasicPlugin.js 文件(獨立模組) // 2、模組對外暴露的 js 函數 class BasicPlugin{ //在構造函數中獲取用戶為該插件傳入的配置 constructor(pluginOptions) { this.options = pluginOptions; } //3、原型定義一個 apply 函數,並注入了 compiler 對象 apply(compiler) { //4、掛載 webpack 事件鉤子(這裡掛載的是 emit 事件) compiler.plugin('emit', function (compilation, callback) { // ... 內部進行自定義的編譯操作 // 5、操作 compilation 對象的內部數據 console.log(compilation); // 6、執行 callback 回調 callback(); }); } } // 7、暴露 js 函數 module.exports = BasicPlugin;
apply方法的由來
因為webpack中調用插件的方式就是plugin.apply()。
webpack部分源碼:
https://github.com/webpack/webpack/blob/10282ea20648b465caec6448849f24fc34e1ba3e/lib/webpack.js#L35
const webpack = (options, callback) => { ... for (const plugin of options.plugins) { plugin.apply(compiler); } ... }
入參Compiler對象解釋
compiler 對象是 webpack 的編譯器對象,compiler 對象會在啟動 webpack 的時候被一次性的初始化,compiler 對象中包含了所有 webpack 可自定義操作的配置,例如 loader 的配置,plugin 的配置,entry 的配置等各種原始 webpack 配置等,在 webpack 插件中的自定義子編譯流程中,我們肯定會用到 compiler 對象中的相關配置資訊,我們相當於可以通過 compiler 對象拿到 webpack 的主環境所有的資訊。webpack部分源碼:
https://github.com/webpack/webpack/blob/10282ea20648b465caec6448849f24fc34e1ba3e/lib/webpack.js#L30
// webpack/lib/webpack.js const Compiler = require("./Compiler") const webpack = (options, callback) => { ... options = new WebpackOptionsDefaulter().process(options) // 初始化 webpack 各配置參數 let compiler = new Compiler(options.context) // 初始化 compiler 對象,這裡 options.context 為 process.cwd() compiler.options = options // 往 compiler 添加初始化參數 new NodeEnvironmentPlugin().apply(compiler)// 往 compiler 添加 Node 環境相關方法 for (const plugin of options.plugins) { plugin.apply(compiler); } ... }
區分Compilation 對象
compilation 實例繼承於 compiler,compilation 對象代表了一次單一的版本 webpack 構建和生成編譯資源(編譯資源是 webpack 通過配置生成的一份靜態資源管理 Map(一切都在記憶體中保存),以 key-value 的形式描述一個 webpack 打包後的文件,編譯資源就是這一個個 key-value 組成的 Map)的過程。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,一次新的編譯將被創建,從而生成一組新的編譯資源以及新的 compilation 對象。一個 compilation 對象包含了 當前的模組資源、編譯生成資源、變化的文件、以及 被跟蹤依賴的狀態資訊。編譯對象也提供了很多關鍵點回調供插件做自定義處理時選擇使用。
Compiler 和 Compilation 的區別在於:Compiler 代表了整個 Webpack 從啟動到關閉的生命周期,而 Compilation 只代表一次新的編譯。
Tapable & Tapable 實例
webpack 的插件架構主要基於 Tapable 實現的,Tapable 是 webpack 項目組的一個內部庫,主要是抽象了一套插件機制。webpack 源程式碼中的一些 Tapable 實例都繼承或混合了 Tapable 類。Tapable 能夠讓我們為 javaScript 模組添加並應用插件。它可以被其它模組繼承或混合。它類似於 NodeJS 的 EventEmitter 類,專註於自定義事件的觸發和操作。除此之外, Tapable 允許你通過回調函數的參數訪問事件的生產者。
簡單來說,就是我們熟悉的發布-訂閱模式。這裡暫不詳細講了,有點偏題~感興趣的可以自行查找資料,或者私聊我~
Plugin調用流程
- 註冊,類似於 EventEmitter 的 on
對應源碼:
https://github.com/webpack/tapable/blob/42b520760e138c23e7808881cb4322557e878307/lib/Tapable.js#L35
compiler.plugin('emit', (compilation, callback) => { // 在生成資源並輸出到目錄之前完成某些邏輯 }) // Tapable.js options => { ... if(hook !== undefined) { const tapOpt = { name: options.fn.name || "unnamed compat plugin", stage: options.stage || 0 }; if(options.async) // 將插件中非同步鉤子的回調函數注入 hook.tapAsync(tapOpt, options.fn); else hook.tap(tapOpt, options.fn); return true; } };
2. 執行,類似於 EventEmitter 的emit
對應源碼:
https://github.com/webpack/webpack/blob/e7c8fa414b718ac98d94a96e2553faceabfbc92f/lib/Compiler.js#L307
this.hooks.emit.callAsync(compilation, err => { if (err) return callback(err); outputPath = compilation.getPath(this.outputPath); this.outputFileSystem.mkdirp(outputPath, emitFiles); });
開發調試Plugin&Loader
npm link
適合場景:Plugin&Loader
Npm link 專門用於開發和調試本地的 Npm 模組,能做到在不發布模組的情況下, 將本地的一個正在開發的模組的源碼鏈接到項目的 node_modules 目錄下,讓項目可以直接使 用本地的 Npm 模組。由於是通過軟鏈接的方式實現的,編輯了本地的 Npm 模組的程式碼,所以在項目中也能使用到編輯後的程式碼。
- 確保正在開發的本地 Loader 模組的 package.json 已經配置好(最主要的main欄位的入口文件指向要正確)
- 在本地的 Npm 模組根目錄下執行 npm link,將本地模組註冊到全局
- 在項目根目錄下執行 npm link loader-name ,將第 2 步註冊到全局的本地 Npm 模組鏈接到項目的 node moduels 下,其中的 loader-name 是指在第 1 步的package.json 文件中配置的模組名稱
Resolveloader
適合場景:loader
ResolveLoader 用於配置 Webpack 如何尋找 Loader ,它在默認情 況下只會去 node_modules 目錄下尋找。為了讓 Webpack 載入放在本地項目中的 Loader, 需要修改 resolveLoader.modules。
//假設本地項目中的 Loader 在項目目錄的 ./loaders/loader-name 下 module.exports = { resolveLoader:{ //去哪些目錄下尋找 Loader ,有先後順序之分 modules :['node modules','./loaders/'], } }