淺談Webpack4 plugins 實現原理
前言
在 wabpack 中核心功能除瞭 loader 應該就是 plugins 插件瞭,它是在webpack執行過程中會廣播一系列事件,plugin 會監聽這些事件並通過 webpack Api 對輸出文件做對應的處理, 如 hmlt-webpack-plugin 就是對模板魔劍 index.html 進行拷貝到 dist 目錄的
認識
先來通過源碼來認識一下 plugins 的基本結構
https://github.com/webpack/webpack/blob/webpack-4/lib/Compiler.js 551行
// 創建一個編譯器 createChildCompiler( compilation, compilerName, compilerIndex, outputOptions, plugins // 裡邊就有包含插件 ) { // new 一個 編譯器 const childCompiler = new Compiler(this.context); // 尋找存在的所有 plugins 插件 if (Array.isArray(plugins)) { for (const plugin of plugins) { // 如果存在, 就調用 plugin 的 apply 方法 plugin.apply(childCompiler); } } // 遍歷尋找 plugin 對應的 hooks for (const name in this.hooks) { if ( ![ "make", "compile", "emit", "afterEmit", "invalid", "done", "thisCompilation" ].includes(name) ) { // 找到對應的 hooks 並調用, if (childCompiler.hooks[name]) { childCompiler.hooks[name].taps = this.hooks[name].taps.slice(); } } } // .... 省略 .... return childCompiler; }
通過上述源碼可以看出來 plugin 本質就是一個類, 首先就是 new 一個 compiler 類,傳入當前的上下文,然後判斷是否存在,存在則直接調用對應 plugin 的 apply 方法,然後再找到對應 plugin 調用的 hooks 事件流 , 發射給對應 hooks 事件
hooks 哪裡來的 ?
https://github.com/webpack/webpack/blob/webpack-4/lib/Compiler.js 42行
// 上述的 Compiler 類繼承自 Tapable 類,而 Tapable 就定義瞭這些 hooks 事件流 class Compiler extends Tapable { constructor(context) { super(); this.hooks = { /** @type {SyncBailHook<Compilation>} */ shouldEmit: new SyncBailHook(["compilation"]), /** @type {AsyncSeriesHook<Stats>} */ done: new AsyncSeriesHook(["stats"]), /** @type {AsyncSeriesHook<>} */ additionalPass: new AsyncSeriesHook([]), /** @type {AsyncSeriesHook<Compiler>} */ beforeRun: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook<Compiler>} */ run: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook<Compilation>} */ emit: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook<string, Buffer>} */ assetEmitted: new AsyncSeriesHook(["file", "content"]), /** @type {AsyncSeriesHook<Compilation>} */ afterEmit: new AsyncSeriesHook(["compilation"]), /** @type {SyncHook<Compilation, CompilationParams>} */ thisCompilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook<Compilation, CompilationParams>} */ compilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook<NormalModuleFactory>} */ normalModuleFactory: new SyncHook(["normalModuleFactory"]), /** @type {SyncHook<ContextModuleFactory>} */ contextModuleFactory: new SyncHook(["contextModulefactory"]), /** @type {AsyncSeriesHook<CompilationParams>} */ beforeCompile: new AsyncSeriesHook(["params"]), /** @type {SyncHook<CompilationParams>} */ compile: new SyncHook(["params"]), /** @type {AsyncParallelHook<Compilation>} */ make: new AsyncParallelHook(["compilation"]), /** @type {AsyncSeriesHook<Compilation>} */ afterCompile: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook<Compiler>} */ watchRun: new AsyncSeriesHook(["compiler"]), /** @type {SyncHook<Error>} */ failed: new SyncHook(["error"]), /** @type {SyncHook<string, string>} */ invalid: new SyncHook(["filename", "changeTime"]), /** @type {SyncHook} */ watchClose: new SyncHook([]), /** @type {SyncBailHook<string, string, any[]>} */ infrastructureLog: new SyncBailHook(["origin", "type", "args"]), // TODO the following hooks are weirdly located here // TODO move them for webpack 5 /** @type {SyncHook} */ environment: new SyncHook([]), /** @type {SyncHook} */ afterEnvironment: new SyncHook([]), /** @type {SyncHook<Compiler>} */ afterPlugins: new SyncHook(["compiler"]), /** @type {SyncHook<Compiler>} */ afterResolvers: new SyncHook(["compiler"]), /** @type {SyncBailHook<string, Entry>} */ entryOption: new SyncBailHook(["context", "entry"]) }; // TODO webpack 5 remove this this.hooks.infrastructurelog = this.hooks.infrastructureLog; // 通過 tab 調用對應的 comiler 編譯器,並傳入一個回調函數 this._pluginCompat.tap("Compiler", options => { switch (options.name) { case "additional-pass": case "before-run": case "run": case "emit": case "after-emit": case "before-compile": case "make": case "after-compile": case "watch-run": options.async = true; break; } }); // 下方省略 ...... }
好瞭,瞭解過基本的結構之後,就可以推理出 plugin 基本的結構和用法瞭,就是下邊這樣
// 定義一個 plugins 類 class MyPlugins { // 上邊有說 new 一個編譯器實例,會執行實例的 apply 方法,傳入對應的 comiler 實例 apply (compiler) { // 調用 new 出來 compiler 實例下的 hooks 事件流,通過 tab 觸發,並接收一個回調函數 compiler.hooks.done.tap('一般為插件昵稱', (默認接收參數) => { console.log('進入執行體'); }) } } // 導出 module.exports = MyPlugins
ok, 以上就是一個簡單的 模板 ,我們來試試內部的鉤子函數,是否會如願以償的被調用和觸發
配置 webpack
let path = require('path') let DonePlugin = require('./plugins/DonePlugins') let AsyncPlugins = require('./plugins/AsyncPlugins') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'build.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new DonePlugin(), // 內部同步 hooks new AsyncPlugins() // 內部異步 hooks ] }
同步 plugin 插件模擬調用
class DonePlugins { apply (compiler) { compiler.hooks.done.tap('DonePlugin', (stats) => { console.log('執行: 編譯完成'); }) } } module.exports = DonePlugins
異步 plugin 插件模擬調用
class AsyncPlugins { apply (compiler) { compiler.hooks.emit.tapAsync('AsyncPlugin', (complete, callback) => { setTimeout(() => { console.log('執行:文件發射出來'); callback() }, 1000) }) } } module.exports = AsyncPlugins
最後編譯 webpack 可以看到編譯控制臺,分別打印 執行: 編譯完成,執行:文件發射出來,說明這樣是可以調用到 hooks 事件流的,並且可以觸發。
實踐出真知
瞭解過基本結構和使用的方式瞭,現在來手寫一個 plugin 插件,嗯,就來一個文件說明插件吧,我們日常打包,可以打包一個 xxx.md 文件到 dist 目錄,來做一個打包說明,就來是實現這麼一個小功能
文件說明插件
class FileListPlugin { // 初始化,獲取文件的名稱 constructor ({filename}) { this.filename = filename } // 同樣的模板形式,定義 apply 方法 apply (compiler) { compiler.hooks.emit.tap('FileListPlugin', (compilation) => { // assets 靜態資源,可以打印出 compilation 參數,還有很多方法和屬性 let assets = compilation.assets; // 定義輸出文檔結構 let content = `## 文件名 資源大小\r\n` // 遍歷靜態資源,動態組合輸出內容 Object.entries(assets).forEach(([filename, stateObj]) => { content += `- ${filename} ${stateObj.size()}\r\n` }) // 輸出資源對象 assets[this.filename] = { source () { return content; }, size () { return content.length } } }) } } // 導出 module.exports = FileListPlugin
webpack 配置
let path = require('path') let HtmlWebpackPlugin = require('html-webpack-plugin') // plugins 目錄與node_modules 同級, 自定義 plugins , 與 loader 類似 let FileListPlugin = require('./plugins/FileListPlugin') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'build.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', filename: 'index.html' }), new FileListPlugin({ filename: 'list.md' }) ] }
ok,通過以上配置,我們再打包的時候就可以看到,每次打包在 dist 目錄就會出現一個 xxx.md 文件,而這個文件的內容就是我們上邊的 content
到此這篇關於淺談Webpack4 plugins 實現原理的文章就介紹到這瞭,更多相關Webpack4 plugins 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 80行代碼寫一個Webpack插件並發佈到npm
- JavaScript webpack5配置及使用基本介紹
- webpack搭建vue環境時報錯異常解決
- 使用Webpack構建多頁面程序的實現步驟
- webpack cjs運行時分析示例詳解