webpack原理(二)自定義plugin

概覽

Webpack 通過 Plugin 機制讓其更加靈活,以適應各種應用場景。 在 Webpack 運行的生命週期中會廣播出許多事件,Plugin 可以監聽這些事件,在合適的時機通過 Webpack 提供的 API 改變輸出結果。
我們先看一個最基礎的 Plugin 的代碼例子:

class BasicPlugin{
  // 在構造函數中獲取用戶給該插件傳入的配置
  constructor(options){
  }

  // Webpack 會調用 BasicPlugin 實例的 apply 方法給插件實例傳入 compiler 對象
  apply(compiler){
    compiler.plugin('compilation',function(compilation) {
    })
  }
}

// 導出 Plugin
module.exports = BasicPlugin;

plugin的使用:

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}

插件的執行過程

  1. 執行 new BasicPlugin(options) 初始化一個 BasicPlugin 獲得其實例。
  2. 初始化 compiler 對象後,再調用 basicPlugin.apply(compiler) 給插件實例傳入 compiler 對象
  3. 獲取到 compiler 對象後,就可以通過 compiler.plugin(事件名稱, 回調函數) 監聽到 Webpack 廣播出來的事件,並且可以通過 compiler 對象去操作 Webpack。

Compiler 和 Compilation

在開發 Plugin 時最常用的兩個對象就是 Compiler 和 Compilation,它們是 Plugin 和 Webpack 之間的橋樑。 Compiler 和 Compilation 的含義如下:

  • Compiler 對象包含了 Webpack 環境所有的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啓動時候被實例化,它是全局唯一的,可以簡單地把它理解爲 Webpack 實例;
  • Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack 以開發模式運行時,每當檢測到一個文件變化,一次新的 Compilation 將被創建。Compilation 對象也提供了很多事件回調供插件做擴展。通過 Compilation 也能讀取到 Compiler 對象。

Compiler 和 Compilation 的區別在於:Compiler 代表了整個 Webpack 從啓動到關閉的生命週期,而 Compilation 只是代表了一次新的編譯。
compiler事件鉤子:
在這裏插入圖片描述
compilation事件鉤子:
在這裏插入圖片描述
上面列舉了compiler和compilation的主要鉤子函數,詳細信息可以查看plugin API文檔。

事件流

Webpack 的事件流機制應用了觀察者模式,和 Node.js 中的 EventEmitter 非常相似。Compiler 和 Compilation 都繼承自 Tapable,可以直接在 Compiler 和 Compilation 對象上廣播和監聽事件,方法如下:

/**
* 廣播出事件
* event-name 爲事件名稱,注意不要和現有的事件重名
* params 爲附帶的參數
*/
compiler.apply('event-name',params);

/**
* 監聽名稱爲 event-name 的事件,當 event-name 事件發生時,函數就會被執行。
* 同時函數中的 params 參數爲廣播事件時附帶的參數。
*/
compiler.plugin('event-name',function(params) {

});

同理,compilation.apply 和 compilation.plugin 使用方法和上面一致。
注意:

  • 只要能拿到 Compiler 或 Compilation 對象,就能廣播出新的事件,所以在新開發的插件中也能廣播出事件,給其它插件監聽使用。
  • 傳給每個插件的 Compiler 和 Compilation 對象都是同一個引用。也就是說在一個插件中修改了 Compiler 或 Compilation 對象上的屬性,會影響到後面的插件。
  • 有些事件是異步的,這些異步的事件會附帶兩個參數,第二個參數爲回調函數,在插件處理完任務時需要調用回調函數通知 Webpack,纔會進入下一處理流程。例如:
 compiler.plugin('emit',function(compilation, callback) {
    // 支持處理邏輯

    // 處理完畢後執行 callback 以通知 Webpack 
    // 如果不執行 callback,運行流程將會一直卡在這不往下執行 
    callback();
  });

常用API

讀取assets, chunks, modules, 以及dependencies

class Plugin {
  apply(compiler) {
    compiler.plugin('emit', function (compilation, callback) {
      // compilation.chunks 存放所有代碼塊,是一個數組
      compilation.chunks.forEach(function (chunk) {
        // chunk 代表一個代碼塊
        // 代碼塊由多個模塊組成,通過 chunk.forEachModule 能讀取組成代碼塊的每個模塊
        chunk.forEachModule(function (module) {
          // module 代表一個模塊
          // module.fileDependencies 存放當前模塊的所有依賴的文件路徑,是一個數組
          module.fileDependencies.forEach(function (filepath) {
          });
        });

        // Webpack 會根據 Chunk 去生成輸出的文件資源,每個 Chunk 都對應一個及其以上的輸出文件
        // 例如在 Chunk 中包含了 CSS 模塊並且使用了 ExtractTextPlugin 時,
        // 該 Chunk 就會生成 .js 和 .css 兩個文件
        chunk.files.forEach(function (filename) {
          // compilation.assets 存放當前所有即將輸出的資源
          // 調用一個輸出資源的 source() 方法能獲取到輸出資源的內容
          let source = compilation.assets[filename].source();
        });
      });

      // 這是一個異步事件,要記得調用 callback 通知 Webpack 本次事件監聽處理結束。
      // 如果忘記了調用 callback,Webpack 將一直卡在這裏而不會往後執行。
      callback();
    })
  }
}

修改輸出資源

有些場景下插件需要修改、增加、刪除輸出的資源,要做到這點需要監聽 emit 事件,因爲發生 emit 事件時所有模塊的轉換和代碼塊對應的文件已經生成好, 需要輸出的資源即將輸出,因此 emit 事件是修改 Webpack 輸出資源的最後時機。
所有需要輸出的資源會存放在 compilation.assets 中,compilation.assets 是一個鍵值對,鍵爲需要輸出的文件名稱,值爲文件對應的內容。
下面我們來看個例子:

class FileListPlugin {
    apply(compiler) {
        compiler.plugin("emit", function(compilation, callback) {
            var filelist = "In this build:\n\n";

            // 循環得到所有文件名
            for (var filename in compilation.assets) {
                filelist += "- " + filename + "\n";
            }
            const plugins = compiler.options.plugins; //所有插件名
            filelist += ` all plugins: \n\n ${plugins}`;
            // 將文件名和插件名插入到新生成的文件中
            compilation.assets["filelist.md"] = {
                source: function() {
                    return filelist;
                },
                size: function() {
                    return filelist.length;
                }
            };

            callback();
        });
    }
}

這裏生成了一個markdown文件,內容是讀取所有的資源文件名。
如果想要讀取 compilation.assets ,代碼如下:

compiler.plugin('emit', (compilation, callback) => {
  // 讀取名稱爲 fileName 的輸出資源
  const asset = compilation.assets[fileName];
  // 獲取輸出資源的內容
  asset.source();
  // 獲取輸出資源的文件大小
  asset.size();
  callback();
});

監聽文件變化

Webpack 會從配置的入口模塊出發,依次找出所有的依賴模塊,當入口模塊或者其依賴的模塊發生變化時, 就會觸發一次新的 Compilation。

在開發插件時經常需要知道是哪個文件發生變化導致了新的 Compilation,爲此可以使用如下代碼:

// 當依賴的文件發生變化時會觸發 watch-run 事件
compiler.plugin('watch-run', (watching, callback) => {
    // 獲取發生變化的文件列表
    const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
    // changedFiles 格式爲鍵值對,鍵爲發生變化的文件路徑。
    if (changedFiles[filePath] !== undefined) {
      // filePath 對應的文件發生了變化
    }
    callback();
});
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章