淺談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!

推薦閱讀: