通過前面章節(jié)內(nèi)容的講解,對于 Webpack 的插件應該已經(jīng)不陌生了,而且對于 Webpack 很多高級的知識點應該都有了一定的了解,包括 Webpack 中的 Compiler 和 Compilation 對象,以及 Webpack 的插件原理。
在本章節(jié)中,主要以官網(wǎng)提供的例子 FileListPlugin/HelloWorldPlugin 來說明如何寫一個插件,而這部分內(nèi)容在前面應該已經(jīng)都有了深入的了解。同時,在本章節(jié)中也會給出 Webpack 中不同插件的類型與區(qū)別。但是,如果想要寫一個自己的 Webpack 的復雜插件,那么除了前面的內(nèi)容以外,也要注意日常的積累??。
如何寫一個 Webpack 的插件
Webpack 的插件機制將 Webpack 引擎的能力暴露給了開發(fā)者,使用 Webpack 內(nèi)置的各種打包階段鉤子函數(shù)使得開發(fā)者能夠引入他們自己的打包流程。寫一個 Webpack 插件往往比寫一個 Loader 復雜,因為需要了解 Webpack 內(nèi)部很多細節(jié)的部分。
如何創(chuàng)建一個 Webpack 的插件
通過前面的章節(jié)內(nèi)容應該有所了解,一個 Webpack 的插件其實包含以下幾個條件:
1、一個 js 命名函數(shù)。
2、在原型鏈上存在一個 apply 方法。
3、為該插件指定一個 Webpack 的事件鉤子函數(shù)。
4、使用 Webpack 內(nèi)部的實例對象(Compiler 或者 Compilation)具有的屬性或者方法。
5、當功能完成以后,需要執(zhí)行 Webpack 的回調(diào)函數(shù)。
比如下面的函數(shù)就具備了上面的條件,所以它是可以作為一個 Webpack 插件的:
function MyExampleWebpackPlugin() {
};
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
//我們主要關(guān)注 compilation 階段,即 webpack 打包階段
compiler.plugin('compilation', function(compilation , callback) {
console.log("This is an example plugin!!!");
//當該插件功能完成以后一定要注意回調(diào) callback 函數(shù)
callback();
});
};
Compiler 和 Compilation 實例
在前面的章節(jié)中已經(jīng)深入講解了這部分的內(nèi)容,我們下面總結(jié)性的給出兩個對象的作用。
Compiler 對象: Compiler 對象代表了 Webpack 完整的可配置的環(huán)境。該對象在 Webpack 啟動的時候會被創(chuàng)建,同時該對象也會被傳入一些可控的配置,如 Options、Loaders、Plugins。當插件被實例化的時候,會收到一個 Compiler 對象,通過這個對象可以訪問 Webpack 的內(nèi)部環(huán)境。
Compilation 對象: Compilation 對象在每次文件變化的時候都會被創(chuàng)建,因此會重新產(chǎn)生新的打包資源。該對象表示本次打包的模塊、編譯的資源、文件改變和監(jiān)聽的依賴文件的狀態(tài)。而且該對象也會提供很多的回調(diào)點,我們的插件可以使用它來完成特定的功能,而提供的鉤子函數(shù)在前面的章節(jié)已經(jīng)講過了,此處不再贅述
Hello World 插件
比如下面是我們寫的一個插件:
//插件內(nèi)部可以接受到該插件的配置參數(shù)
function HelloWorldPlugin(options) {
}
HelloWorldPlugin.prototype.apply = function(compiler) {
//此處利用了 Compiler 提供的 done 鉤子函數(shù),作用前面已經(jīng)說過
compiler.plugin('done', function() {
console.log('Hello World!');
});
};
module.exports = HelloWorldPlugin;
那么在 Webpack 配置文件中就可以通過下面的方式來進行配置:
var HelloWorldPlugin = require('hello-world');
//已經(jīng)發(fā)布到 NPM
var webpackConfig = {
plugins: [
new HelloWorldPlugin({options: true})
]
};
前面已經(jīng)說過,Webpack 插件最重要的就是 Compilation 和 Compiler 對象。先來看看在插件里面如何使用 Compilation 對象:
function HelloCompilationPlugin(options) {}
HelloCompilationPlugin.prototype.apply = function(compiler) {
//使用 Compiler 對象的 compilation 鉤子函數(shù)就可以獲取 Compilation 對象
compiler.plugin("compilation", function(compilation) {
//使用 Compilation 注冊回調(diào)
compilation.plugin("optimize", function() {
console.log("Assets are being optimized.");
});
});
};
module.exports = HelloCompilationPlugin;
異步插件
上面看到的 HelloWorld 插件是同步的,還有一種插件是異步的,來看看異步插件如何編寫:
function HelloAsyncPlugin(options) {}
HelloAsyncPlugin.prototype.apply = function(compiler) {
compiler.plugin("emit", function(compilation, callback) {
// Do something async...
setTimeout(function() {
console.log("Done with async work...");
callback();
}, 1000);
});
};
module.exports = HelloAsyncPlugin;
從這里可看出,異步插件和同步插件最大的不同在于,異步插件會傳入一個 callback 參數(shù),當插件完成相應的功能以后,必須回調(diào) callback() 函數(shù)。
當訪問到 Webpack 的 Compiler 和每次產(chǎn)生的 Compilation 對象的時候,可以使用 Webpack 的引擎來完成任何事情??梢灾匦绿幚硪呀?jīng)存在的文件,創(chuàng)建自己的派生文件(想要多產(chǎn)生的文件),或者對將要產(chǎn)生的資源進行修改(HtmlWebpackPlugin)等等。例如,在前面章節(jié)就已經(jīng)講述的下面的實例,該實例就是有效的利用了 Compiler 的文件輸出 emit 階段產(chǎn)生我們自己需要的文件:
function FileListPlugin(options) {}
FileListPlugin.prototype.apply = function(compiler) {
compiler.plugin('emit', function(compilation, callback) {
var filelist = 'In this build:\n\n';
//compilation.assets 和 compilation.chunks 前面已經(jīng)說過
for (var filename in compilation.assets) {
filelist += ('- '+ filename +'\n');
}
//在 compilation.assets 中添加需要的資源
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback();
});
};
module.exports = FileListPlugin;
Webpack 的插件類型
插件可以根據(jù)它注冊的事件分成不同的類型。每一個特定的鉤子函數(shù)決定了它會被如何執(zhí)行,比如插件可以分為如下的類型。
同步插件
此時 Tapable 實例通過下面的方式來執(zhí)行插件:
applyPlugins(name: string, args: any...)
//或者
applyPluginsBailResult(name: string, args: any...)
這意味著每一個插件的回調(diào)函數(shù)將會被按照順序依次執(zhí)行(觀察者模式),并傳入特定的參數(shù) args,這是插件的最簡單的格式。很多有用的鉤子函數(shù)如"compile"、"this-compilation"都期望每一個插件同步執(zhí)行。下面給出 Webpack 對于 compile 這個鉤子函數(shù)的執(zhí)行方式:
Compiler.prototype.compile = function(callback) {
self.applyPluginsAsync("before-compile", params, function(err) {
self.applyPlugins("compile", params);
//執(zhí)行 compile 階段,同步執(zhí)行插件的方式
var compilation = self.newCompilation(params);
self.applyPluginsParallel("make", compilation, function(err) {
compilation.finish();
compilation.seal(function(err) {
self.applyPluginsAsync("after-compile", compilation, function(err) {
});
});
});
});
};
瀑布流插件
這種類型的插件通過下面的方法來執(zhí)行:
applyPluginsWaterfall(name: string, init: any, args: any...)
此時,每一個插件都會將前一個插件的返回值作為參數(shù)輸入,并傳入自己的參數(shù),這種插件必須考慮插件的執(zhí)行順序。第一個插件傳入的第二個參數(shù)值為 init,而最后一個插件的返回值作為 applyPluginsWaterfall 的返回值。這種插件的模式常用于 Webpack 的模板,如 ModuleTemplate、ChunkTemplate。比如 ModuleTemplate 下就使用了如下的內(nèi)容:
const Template = require("./Template");
module.exports = class ModuleTemplate extends Template {
constructor(outputOptions) {
super(outputOptions);
}
render(module, dependencyTemplates, chunk) {
const moduleSource = module.source(dependencyTemplates, this.outputOptions, this.requestShortener);
const moduleSourcePostModule = this.applyPluginsWaterfall("module", moduleSource, module, chunk, dependencyTemplates);
const moduleSourcePostRender = this.applyPluginsWaterfall("render", moduleSourcePostModule, module, chunk, dependencyTemplates);
//1.必須考慮插件的執(zhí)行順序
return this.applyPluginsWaterfall("package", moduleSourcePostRender, module, chunk, dependencyTemplates);
}
updateHash(hash) {
hash.update("1");
this.applyPlugins("hash", hash);
}
};
異步插件
如果插件會被異步執(zhí)行,那么應該使用下面的方式來完成:
applyPluginsAsync(name: string, args: any..., callback: (err?: Error) -> void)
此時插件處理函數(shù)調(diào)用的時候會傳入 args 和簽名為 (err?: Error) -> void 的回調(diào)函數(shù)。我們的處理函數(shù)將會按照注冊時候的順序被執(zhí)行。而回調(diào)函數(shù) callback() 將會在所有的處理函數(shù)被調(diào)用以后調(diào)用。這種模式常常用于如 "emit"、"run"等鉤子函數(shù)。比如下面的 Compiler 的 run 方法的具體邏輯。
self.applyPluginsAsync("run", self, function(err) {
if(err) return callback(err);
self.readRecords(function(err) {
if(err) return callback(err);
//2.調(diào)用compile的回調(diào)函數(shù)
self.compile(function onCompiled(err, compilation) {
//其他代碼邏輯
});
});
});
異步瀑布流插件
此時所有的插件將會被異步執(zhí)行,同時遵循瀑布流的方式。此時以下面的方式來調(diào)用:
applyPluginsAsyncWaterfall(name: string, init: any, callback: (err: Error, result: any) -> void)
此時插件的回調(diào)函數(shù)在調(diào)用的時候傳入當前的值,回調(diào)函數(shù)被調(diào)用的時候會有如下的簽名 (err: Error, nextValue: any) -> void。如果回調(diào)函數(shù)被調(diào)用了,那么 nextValue 就會成為下一個處理函數(shù)的當前值。第一個處理函數(shù)的當前值為 init。當所有的處理函數(shù)都執(zhí)行以后,回調(diào)函數(shù)會傳入最后一個插件的返回值。如果任何一個處理函數(shù)傳入了一個 err,那么回調(diào)函數(shù)將會傳入錯誤參數(shù) err,此時余下的所有的處理函數(shù)都不會被執(zhí)行。這種模式常常用于如 "before-resolve" 或者 "after-resolve"。
Webpack 插件調(diào)用順序
Webpack 的源碼中經(jīng)常會看到上面說的執(zhí)行插件注冊的方法,我們給出下面的 seal 方法的部分代碼:
seal(callback) {
self.applyPlugins0("seal");
self.applyPlugins0("optimize");
while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) ||
self.applyPluginsBailResult1("optimize-modules", self.modules) ||
self.applyPluginsBailResult1("optimize-modules-advanced", self.modules));
self.applyPlugins1("after-optimize-modules", self.modules);
//這里是 optimize module
while(self.applyPluginsBailResult1("optimize-chunks-basic", self.chunks) ||
self.applyPluginsBailResult1("optimize-chunks", self.chunks) ||
self.applyPluginsBailResult1("optimize-chunks-advanced", self.chunks));
//這里是 optimize chunk
self.applyPlugins1("after-optimize-chunks", self.chunks);
//這里是 optimize tree
self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) {
self.applyPlugins2("after-optimize-tree", self.chunks, self.modules);
const shouldRecord = self.applyPluginsBailResult("should-record") !== false;
self.applyPlugins2("revive-modules", self.modules, self.records);
self.applyPlugins1("optimize-module-order", self.modules);
self.applyPlugins1("advanced-optimize-module-order", self.modules);
self.applyPlugins1("before-module-ids", self.modules);
self.applyPlugins1("module-ids", self.modules);
self.applyModuleIds();
self.applyPlugins1("optimize-module-ids", self.modules);
self.applyPlugins1("after-optimize-module-ids", self.modules);
self.sortItemsWithModuleIds();
self.applyPlugins2("revive-chunks", self.chunks, self.records);
self.applyPlugins1("optimize-chunk-order", self.chunks);
self.applyPlugins1("before-chunk-ids", self.chunks);
self.applyChunkIds();
self.applyPlugins1("optimize-chunk-ids", self.chunks);
self.applyPlugins1("after-optimize-chunk-ids", self.chunks);
self.sortItemsWithChunkIds();
if(shouldRecord)
self.applyPlugins2("record-modules", self.modules, self.records);
if(shouldRecord)
self.applyPlugins2("record-chunks", self.chunks, self.records);
self.applyPlugins0("before-hash");
self.createHash();
self.applyPlugins0("after-hash");
if(shouldRecord)
self.applyPlugins1("record-hash", self.records);
self.applyPlugins0("before-module-assets");
self.createModuleAssets();
if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) {
self.applyPlugins0("before-chunk-assets");
self.createChunkAssets();
}
self.applyPlugins1("additional-chunk-assets", self.chunks);
self.summarizeDependencies();
if(shouldRecord)
self.applyPlugins2("record", self, self.records);
self.applyPluginsAsync("additional-assets", err => {
if(err) {
return callback(err);
}
self.applyPluginsAsync("optimize-chunk-assets", self.chunks, err => {
if(err) {
return callback(err);
}
self.applyPlugins1("after-optimize-chunk-assets", self.chunks);
self.applyPluginsAsync("optimize-assets", self.assets, err => {
if(err) {
return callback(err);
}
self.applyPlugins1("after-optimize-assets", self.assets);
if(self.applyPluginsBailResult("need-additional-seal")) {
self.unseal();
return self.seal(callback);
}
return self.applyPluginsAsync("after-seal", callback);
});
});
});
});
}
而各個鉤子函數(shù)執(zhí)行的順序可以查看下面的內(nèi)容:
'before run'
'run'
compile:func//調(diào)用 compile() 函數(shù)
'before compile'
'compile'//(1)compiler 對象的第一階段
newCompilation:object//創(chuàng)建 compilation 對象
'make' //(2)compiler 對象的第二階段
compilation.finish:func
"finish-modules"
compilation.seal
"seal"
"optimize"
"optimize-modules-basic"
"optimize-modules-advanced"
"optimize-modules"
"after-optimize-modules"http://首先是優(yōu)化模塊
"optimize-chunks-basic"
"optimize-chunks"http://然后是優(yōu)化 chunk
"optimize-chunks-advanced"
"after-optimize-chunks"
"optimize-tree"
"after-optimize-tree"
"should-record"
"revive-modules"
"optimize-module-order"
"advanced-optimize-module-order"
"before-module-ids"
"module-ids"http://首先優(yōu)化 module-order,然后優(yōu)化 module-id
"optimize-module-ids"
"after-optimize-module-ids"
"revive-chunks"
"optimize-chunk-order"
"before-chunk-ids"http://首先優(yōu)化 chunk-order,然后 chunk-id
"optimize-chunk-ids"
"after-optimize-chunk-ids"
"record-modules"http://record module 然后 record chunk
"record-chunks"
"before-hash"
compilation.createHash//func
"chunk-hash"http://webpack-md5-hash
"after-hash"
"record-hash"http://before-hash/after-hash/record-hash
"before-module-assets"
"should-generate-chunk-assets"
"before-chunk-assets"
"additional-chunk-assets"
"record"
"additional-assets"
"optimize-chunk-assets"
"after-optimize-chunk-assets"
"optimize-assets"
"after-optimize-assets"
"need-additional-seal"
unseal:func
"unseal"
"after-seal"
"after-compile"http://(4)完成模塊構(gòu)建和編譯過程(seal 函數(shù)回調(diào))
"emit"http://(5)compile 函數(shù)的回調(diào),compiler 開始輸出 assets,是改變 assets 最后機會
"after-emit"http://(6)文件產(chǎn)生完成