概覽
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),
]
}
插件的執行過程
- 執行 new BasicPlugin(options) 初始化一個 BasicPlugin 獲得其實例。
- 初始化 compiler 對象後,再調用 basicPlugin.apply(compiler) 給插件實例傳入 compiler 對象
- 獲取到 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();
});