一. webpack打包流程
開發 Webpack 插件的第一步,就是明確:我的插件要接入 Webpack 構建流程的哪個階段,解決什么問題。
- 了解流程之前首先要了解插件的兩個核心概念:compiler,compilation
1. compiler:全局構建控制器
你可以這樣理解它:
- 是 Webpack 的“大腦”,負責整個構建流程
- 只會在整個 Webpack 啟動時創建一次
- 提供入口:如 compiler.hooks.compile、compiler.hooks.run 等
注冊插件
class MyPlugin {apply(compiler) {// 插件注冊時調用一次}
}
使用鉤子
compiler.hooks.beforeRun.tap('MyPlugin', () => { ... })
2. compilation:本次構建上下文
你可以這樣理解它:
- 是每次構建過程中的“局部快照”
- 管理模塊 (module)、代碼塊 (chunk)、資源 (asset)等
- 在監聽模式或熱更新時,每次都會重新生成一個 compilation 實例
你要修改文件,生成資源,訪問chunk的時候,一定是在 compilation 階段完成的
compiler.hooks.thisCompilation.tap('MyPlugin', (compilation) => {// compilation 是這一次構建的上下文compilation.hooks.buildModule.tap(...)compilation.hooks.processAssets.tap(...)
});
舉個例子
class FileListPlugin {apply(compiler) {// 第一次執行,僅執行一次,注冊插件鉤子compiler.hooks.thisCompilation.tap('FileListPlugin', (compilation) => {// 每次構建及熱更新都會觸發compilation.hooks.processAssets.tap({name: 'FileListPlugin',stage: compilation.constructor.PROCESS_ASSETS_STAGE_SUMMARIZE},() => {const files = Object.keys(compilation.assets).join('\n');compilation.emitAsset('filelist.md', new compiler.webpack.sources.RawSource(files));});});}
}
webpack打包流程
階段 | 對應方法 | 插件鉤子 | 說明 |
---|---|---|---|
1?? 初始化 | webpack() 創建 compiler | compiler.hooks.environment 等 | 讀取配置、初始化插件 |
2?? 編譯開始 | compiler.run() | compiler.hooks.beforeRun 、run 、compile | 啟動構建,調用編譯 |
3?? 創建 Compilation | new Compilation() | thisCompilation 、compilation | 代表一次構建,管理模塊、資源 |
4?? 構建模塊 | buildModule/handleModuleCreation | compilation.hooks.buildModule | 構建入口與遞歸依賴 |
5?? 完成模塊 | seal() | seal 、optimizeModules | 完成依賴分析與模塊關系確定 |
6?? 生成代碼塊 | createChunkGraph() | optimizeChunks | 把模塊打成 chunk |
7?? 生成資源 | emitAssets() | processAssets | 將 chunk 渲染為文件(此時文件還在內存中) |
8?? 輸出結果 | outputFileSystem.writeFile | afterEmit 、done | 寫入磁盤,構建完成 |
二. 插件的開發
1. 插件的基本結構
class MyPlugin {constructor(options) {this.options = options;}// 創建插件類并實現 apply(compiler) 方法apply(compiler) {// 注冊 compiler 生命周期鉤子compiler.hooks.thisCompilation.tap('MyPlugin', (compilation) => {// 注冊 compilation 生命周期鉤子compilation.hooks.processAssets.tap({name: 'MyPlugin',stage: compilation.constructor.PROCESS_ASSETS_STAGE_SUMMARIZE},(assets) => {const { RawSource } = compiler.webpack.sources;// 修改、添加資源const content = 'hello plugin';compilation.emitAsset('my-output.txt', new RawSource(content));});});}
}module.exports = MyPlugin;
- stage 是一個數字,標識你要插入的邏輯在整個資源處理生命周期中的階段優先級
- PROCESS_ASSETS_STAGE_SUMMARIZE的stage為7000,代表總結階段,可以做輸出文件列表、日志、清單等操作
- 其他更多的stage可以翻閱webpack官網查看
2. 注冊鉤子的方法
tap: 同步鉤子
compiler.hooks.beforeRun.tap('MyPlugin', (compiler) => {console.log('Before run');
});
tapAsync:異步鉤子(使用回調)
compiler.hooks.beforeRun.tapAsync('MyPlugin', (compiler, callback) => {setTimeout(() => {console.log('Before run (async)');callback();}, 1000);
});
- 用于需要手動處理異步任務的情況,必須調用 callback()
tapPromise:異步鉤子(使用 Promise)
compiler.hooks.beforeRun.tapPromise('MyPlugin', (compiler) => {return new Promise((resolve) => {setTimeout(() => {console.log('Before run (promise)');resolve();}, 1000);});
});
- 推薦用于現代異步開發場景
三. 利用webpack插件實現上線后熱更新的功能
1. 構建完成后自動生成一個帶 hash 的版本號文件
class VersionFilePlugin {constructor(options = {}) {this.filename = options.filename || 'version.txt';}apply(compiler) {compiler.hooks.thisCompilation.tap('VersionFilePlugin', (compilation) => {const { RawSource } = compiler.webpack.sources;compilation.hooks.processAssets.tap({name: 'VersionFilePlugin',stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS},() => {// 獲取當前構建 hashconst hash = compilation.fullHash;const content = `version: ${hash}`;// 添加 version.txt 文件compilation.emitAsset(this.filename, new RawSource(content));});});}
}module.exports = VersionFilePlugin;
注冊插件
const VersionFilePlugin = require('./plugins/VersionFilePlugin');module.exports = {// ...plugins: [new VersionFilePlugin({filename: 'version.txt'})]
};
2. 定時輪詢 version.txt,檢測 hash 是否變化
<script>
(function () {const VERSION_CHECK_INTERVAL = 1000 * 60 * 5; // 每 5 分鐘檢查一次const VERSION_URL = '/version.txt';let currentVersion = null;async function checkVersion() {try {const res = await fetch(VERSION_URL, { cache: 'no-store' });const text = await res.text();const latestVersion = text.trim().split('version: ')[1];if (currentVersion && latestVersion !== currentVersion) {console.warn('[version check] 新版本已發布,自動刷新頁面');location.reload(true); // 強制刷新頁面}currentVersion = latestVersion;} catch (err) {console.error('[version check] 檢查失敗:', err);}}// 初始加載checkVersion();// 定時檢查setInterval(checkVersion, VERSION_CHECK_INTERVAL);
})();
</script>
?? 需要將該文件一并部署到 Web 服務器根目錄
有好多全局構建的生命周期鉤子 和 每次構建生成的模塊、資源、chunk 等上下文生命周期鉤子本文沒有展示,想了解更多的可以去看下webpack官方文檔