探寻 webpack 插件机制
webpack 可謂是讓人欣喜又讓人憂,功能強大但需要一定的學習成本。在探尋 webpack 插件機制前,首先需要了解一件有意思的事情,webpack 插件機制是整個 webpack 工具的骨架,而 webpack 本身也是利用這套插件機制構建出來的。因此在深入認識 webpack 插件機制后,再來進行項目的相關優化,想必會大有裨益。
webpack 插件
先來瞅瞅 webpack 插件在項目中的運用
const MyPlugin = require('myplugin') const webpack = require('webpack')webpack({...,plugins: [new MyPlugin()]..., }) 復制代碼那么符合什么樣的條件能作為 webpack 插件呢?一般來說,webpack 插件有以下特點:
獨立的 JS 模塊,暴露相應的函數
函數原型上的 apply 方法會注入 compiler 對象
compiler 對象上掛載了相應的 webpack 事件鉤子
事件鉤子的回調函數里能拿到編譯后的 compilation 對象,如果是異步鉤子還能拿到相應的 callback
下面結合代碼來看看:
function MyPlugin(options) {} // 2.函數原型上的 apply 方法會注入 compiler 對象 MyPlugin.prototype.apply = function(compiler) {// 3.compiler 對象上掛載了相應的 webpack 事件鉤子 4.事件鉤子的回調函數里能拿到編譯后的 compilation 對象compiler.plugin('emit', (compilation, callback) => {...}) } // 1.獨立的 JS 模塊,暴露相應的函數 module.exports = MyPlugin 復制代碼這樣子,webpack 插件的基本輪廓就勾勒出來了,此時疑問點有幾點,
疑問 2:compiler 對象是什么呢?
疑問 3:compiler 對象上的事件鉤子是怎樣的?
疑問 4:事件鉤子的回調函數里能拿到的 compilation 對象又是什么呢?
這些疑問也是本文的線索,讓我們一個個探索。
compiler 對象
compiler 即 webpack 的編輯器對象,在調用 webpack 時,會自動初始化 compiler 對象,源碼如下:
// 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);}... } 復制代碼終上,compiler 對象中包含了所有 webpack 可配置的內容,開發插件時,我們可以從 compiler 對象中拿到所有和 webpack 主環境相關的內容。
compilation 對象
compilation 對象代表了一次單一的版本構建和生成資源。當運行 webpack 時,每當檢測到一個文件變化,一次新的編譯將被創建,從而生成一組新的編譯資源。一個編譯對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。
結合源碼來理解下上面這段話,首先 webpack 在每次執行時會調用 compiler.run() (源碼位置),接著追蹤 onCompiled 函數傳入的 compilation 參數,可以發現 compilation 來自構造函數 Compilation。
// webpack/lib/Compiler.js const Compilation = require("./Compilation");newCompilation(params) {const compilation = new Compilation(this);...return compilation; } 復制代碼不得不提的 tapable 庫
再介紹完 compiler 對象和 compilation 對象后,不得不提的是 tapable 這個庫,這個庫暴露了所有和事件相關的 pub/sub 的方法。而且函數 Compiler 以及函數 Compilation 都繼承自 Tapable。
事件鉤子
事件鉤子其實就是類似 MVVM 框架的生命周期函數,在特定階段能做特殊的邏輯處理。了解一些常見的事件鉤子是寫 webpack 插件的前置條件,下面列舉些常見的事件鉤子以及作用:
| after-plugins | 設置完一組初始化插件之后 | compiler | sync |
| after-resolvers | 設置完 resolvers 之后 | compiler | sync |
| run | 在讀取記錄之前 | compiler | async |
| compile | 在創建新 compilation 之前 | compilationParams | sync |
| compilation | compilation 創建完成 | compilation | sync |
| emit | 在生成資源并輸出到目錄之前 | compilation | async |
| after-emit | 在生成資源并輸出到目錄之后 | compilation | async |
| done | 完成編譯 | stats | sync |
完整地請參閱官方文檔手冊,同時瀏覽相關源碼 也能比較清晰地看到各個事件鉤子的定義。
插件流程淺析
拿 emit 鉤子為例,下面分析下插件調用源碼:
compiler.plugin('emit', (compilation, callback) => {// 在生成資源并輸出到目錄之前完成某些邏輯 }) 復制代碼此處調用的 plugin 函數源自上文提到的 tapable 庫,其最終調用棧指向了 hook.tapAsync(),其作用類似于 EventEmitter 的 on,源碼如下:
// 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); // 將插件中異步鉤子的回調函數注入elsehook.tap(tapOpt, options.fn);return true;} }; 復制代碼有注入必有觸發的地方,源碼中通過 callAsync 方法觸發之前注入的異步事件,callAsync 類似 EventEmitter 的 emit,相關源碼如下:
this.hooks.emit.callAsync(compilation, err => {if (err) return callback(err);outputPath = compilation.getPath(this.outputPath);this.outputFileSystem.mkdirp(outputPath, emitFiles); }); 復制代碼一些深入細節這里就不展開了,說下關于閱讀比較大型項目的源碼的兩點體會,
-
要抓住一條主線索去讀,忽視細節。否則會浪費很多時間而且會有挫敗感;
-
結合調試工具來分析,很多點不用調試工具的話很容易顧此失彼;
動手實現個 webpack 插件
結合上述知識點的分析,不難寫出自己的 webpack 插件,關鍵在于想法。為了統計項目中 webpack 各包的有效使用情況,在 fork webpack-visualizer 的基礎上對代碼升級了一番,項目地址。效果如下:
插件核心代碼正是基于上文提到的 emit 鉤子,以及 compiler 和 compilation 對象。代碼如下:
class AnalyzeWebpackPlugin {constructor(opts = { filename: 'analyze.html' }) {this.opts = opts}apply(compiler) {const self = thiscompiler.plugin("emit", function (compilation, callback) {let stats = compilation.getStats().toJson({ chunkModules: true }) // 獲取各個模塊的狀態let stringifiedStats = JSON.stringify(stats)// 服務端渲染let html = `<!doctype html><meta charset="UTF-8"><title>AnalyzeWebpackPlugin</title><style>${cssString}</style><div id="App"></div><script>window.stats = ${stringifiedStats};</script><script>${jsString}</script>`compilation.assets[`${self.opts.filename}`] = { // 生成文件路徑source: () => html,size: () => html.length}callback()})} } 復制代碼參考資料
看清楚真正的 Webpack 插件
webpack 官網
總結
以上是生活随笔為你收集整理的探寻 webpack 插件机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 傻瓜式的php+mysql伪静态(真实存
- 下一篇: 解决rtl8723be网卡故障