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調用流程

  1. 註冊,類似於 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 模組的程式碼,所以在項目中也能使用到編輯後的程式碼。

  1. 確保正在開發的本地 Loader 模組的 package.json 已經配置好(最主要的main欄位的入口文件指向要正確)
  2. 在本地的 Npm 模組根目錄下執行 npm link,將本地模組註冊到全局
  3. 在項目根目錄下執行 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/'],          }      }