通過前面章節(jié)內(nèi)容的講解蛉拙,對于 Webpack 的插件應(yīng)該已經(jīng)不陌生了岛请,而且對于 Webpack 很多高級的知識點應(yīng)該都有了一定的了解压怠,包括 Webpack 中的 Compiler 和 Compilation 對象掐场,以及 Webpack 的插件原理。
在本章節(jié)中旋膳,主要以官網(wǎng)提供的例子 FileListPlugin/HelloWorldPlugin 來說明如何寫一個插件澎语,而這部分內(nèi)容在前面應(yīng)該已經(jīng)都有了深入的了解。同時验懊,在本章節(jié)中也會給出 Webpack 中不同插件的類型與區(qū)別擅羞。但是,如果想要寫一個自己的 Webpack 的復(fù)雜插件义图,那么除了前面的內(nèi)容以外减俏,也要注意日常的積累??。
如何寫一個 Webpack 的插件
Webpack 的插件機制將 Webpack 引擎的能力暴露給了開發(fā)者歌溉,使用 Webpack 內(nèi)置的各種打包階段鉤子函數(shù)使得開發(fā)者能夠引入他們自己的打包流程垄懂。寫一個 Webpack 插件往往比寫一個 Loader 復(fù)雜骑晶,因為需要了解 Webpack 內(nèi)部很多細節(jié)的部分。
如何創(chuàng)建一個 Webpack 的插件
通過前面的章節(jié)內(nèi)容應(yīng)該有所了解草慧,一個 Webpack 的插件其實包含以下幾個條件:
1桶蛔、一個 js 命名函數(shù)。
2漫谷、在原型鏈上存在一個 apply 方法仔雷。
3、為該插件指定一個 Webpack 的事件鉤子函數(shù)舔示。
4碟婆、使用 Webpack 內(nèi)部的實例對象(Compiler 或者 Compilation)具有的屬性或者方法。
5惕稻、當(dāng)功能完成以后竖共,需要執(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!!!");
//當(dāng)該插件功能完成以后一定要注意回調(diào) callback 函數(shù)
callback();
});
};
Compiler 和 Compilation 實例
在前面的章節(jié)中已經(jīng)深入講解了這部分的內(nèi)容,我們下面總結(jié)性的給出兩個對象的作用蜘渣。
Compiler 對象: Compiler 對象代表了 Webpack 完整的可配置的環(huán)境淌铐。該對象在 Webpack 啟動的時候會被創(chuàng)建,同時該對象也會被傳入一些可控的配置蔫缸,如 Options腿准、Loaders、Plugins拾碌。當(dāng)插件被實例化的時候吐葱,會收到一個 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ù)晚唇,當(dāng)插件完成相應(yīng)的功能以后,必須回調(diào) callback() 函數(shù)盗似。
當(dāng)訪問到 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í)行雪标,那么應(yīng)該使用下面的方式來完成:
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)用的時候傳入當(dāng)前的值,回調(diào)函數(shù)被調(diào)用的時候會有如下的簽名 (err: Error, nextValue: any) -> void竖独。如果回調(diào)函數(shù)被調(diào)用了糙俗,那么 nextValue 就會成為下一個處理函數(shù)的當(dāng)前值。第一個處理函數(shù)的當(dāng)前值為 init预鬓。當(dāng)所有的處理函數(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)生完成