通过前面章节内容的讲解,对于 Webpack 的插件应该已经不陌生了,而且对于 Webpack 很多高级的知识点应该都有了一定的了解,包括 Webpack 中的 Compiler 和 Compilation 对象,以及 Webpack 的插件原理。
在本章节中,主要以官网提供的例子 FileListPlugin/HelloWorldPlugin 来说明如何写一个插件,而这部分内容在前面应该已经都有了深入的了解。同时,在本章节中也会给出 Webpack 中不同插件的类型与区别。但是,如果想要写一个自己的 Webpack 的复杂插件,那么除了前面的内容以外,也要注意日常的积累��。
如何写一个 Webpack 的插件
Webpack 的插件机制将 Webpack 引擎的能力暴露给了开发者,使用 Webpack 内置的各种打包阶段钩子函数使得开发者能够引入他们自己的打包流程。写一个 Webpack 插件往往比写一个 Loader 复杂,因为需要了解 Webpack 内部很多细节的部分。
如何创建一个 Webpack 的插件
通过前面的章节内容应该有所了解,一个 Webpack 的插件其实包含以下几个条件:
1、一个 js 命名函数。
2、在原型链上存在一个 apply 方法。
3、为该插件指定一个 Webpack 的事件钩子函数。
4、使用 Webpack 内部的实例对象(Compiler 或者 Compilation)具有的属性或者方法。
5、当功能完成以后,需要执行 Webpack 的回调函数。
比如下面的函数就具备了上面的条件,所以它是可以作为一个 Webpack 插件的:
function MyExampleWebpackPlugin() {
};
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
//我们主要关注 compilation 阶段,即 webpack 打包阶段
compiler.plugin('compilation', function(compilation , callback) {
console.log("This is an example plugin!!!");
//当该插件功能完成以后一定要注意回调 callback 函数
callback();
});
};
Compiler 和 Compilation 实例
在前面的章节中已经深入讲解了这部分的内容,我们下面总结性的给出两个对象的作用。
Compiler 对象: Compiler 对象代表了 Webpack 完整的可配置的环境。该对象在 Webpack 启动的时候会被创建,同时该对象也会被传入一些可控的配置,如 Options、Loaders、Plugins。当插件被实例化的时候,会收到一个 Compiler 对象,通过这个对象可以访问 Webpack 的内部环境。
Compilation 对象: Compilation 对象在每次文件变化的时候都会被创建,因此会重新产生新的打包资源。该对象表示本次打包的模块、编译的资源、文件改变和监听的依赖文件的状态。而且该对象也会提供很多的回调点,我们的插件可以使用它来完成特定的功能,而提供的钩子函数在前面的章节已经讲过了,此处不再赘述
Hello World 插件
比如下面是我们写的一个插件:
//插件内部可以接受到该插件的配置参数
function HelloWorldPlugin(options) {
}
HelloWorldPlugin.prototype.apply = function(compiler) {
//此处利用了 Compiler 提供的 done 钩子函数,作用前面已经说过
compiler.plugin('done', function() {
console.log('Hello World!');
});
};
module.exports = HelloWorldPlugin;
那么在 Webpack 配置文件中就可以通过下面的方式来进行配置:
var HelloWorldPlugin = require('hello-world');
//已经发布到 NPM
var webpackConfig = {
plugins: [
new HelloWorldPlugin({options: true})
]
};
前面已经说过,Webpack 插件最重要的就是 Compilation 和 Compiler 对象。先来看看在插件里面如何使用 Compilation 对象:
function HelloCompilationPlugin(options) {}
HelloCompilationPlugin.prototype.apply = function(compiler) {
//使用 Compiler 对象的 compilation 钩子函数就可以获取 Compilation 对象
compiler.plugin("compilation", function(compilation) {
//使用 Compilation 注册回调
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 参数,当插件完成相应的功能以后,必须回调 callback() 函数。
当访问到 Webpack 的 Compiler 和每次产生的 Compilation 对象的时候,可以使用 Webpack 的引擎来完成任何事情。可以重新处理已经存在的文件,创建自己的派生文件(想要多产生的文件),或者对将要产生的资源进行修改(HtmlWebpackPlugin)等等。例如,在前面章节就已经讲述的下面的实例,该实例就是有效的利用了 Compiler 的文件输出 emit 阶段产生我们自己需要的文件:
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 前面已经说过
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 的插件类型
插件可以根据它注册的事件分成不同的类型。每一个特定的钩子函数决定了它会被如何执行,比如插件可以分为如下的类型。
同步插件
此时 Tapable 实例通过下面的方式来执行插件:
applyPlugins(name: string, args: any...)
//或者
applyPluginsBailResult(name: string, args: any...)
这意味着每一个插件的回调函数将会被按照顺序依次执行(观察者模式),并传入特定的参数 args,这是插件的最简单的格式。很多有用的钩子函数如"compile"、"this-compilation"都期望每一个插件同步执行。下面给出 Webpack 对于 compile 这个钩子函数的执行方式:
Compiler.prototype.compile = function(callback) {
self.applyPluginsAsync("before-compile", params, function(err) {
self.applyPlugins("compile", params);
//执行 compile 阶段,同步执行插件的方式
var compilation = self.newCompilation(params);
self.applyPluginsParallel("make", compilation, function(err) {
compilation.finish();
compilation.seal(function(err) {
self.applyPluginsAsync("after-compile", compilation, function(err) {
});
});
});
});
};
瀑布流插件
这种类型的插件通过下面的方法来执行:
applyPluginsWaterfall(name: string, init: any, args: any...)
此时,每一个插件都会将前一个插件的返回值作为参数输入,并传入自己的参数,这种插件必须考虑插件的执行顺序。第一个插件传入的第二个参数值为 init,而最后一个插件的返回值作为 applyPluginsWaterfall 的返回值。这种插件的模式常用于 Webpack 的模板,如 ModuleTemplate、ChunkTemplate。比如 ModuleTemplate 下就使用了如下的内容:
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.必须考虑插件的执行顺序
return this.applyPluginsWaterfall("package", moduleSourcePostRender, module, chunk, dependencyTemplates);
}
updateHash(hash) {
hash.update("1");
this.applyPlugins("hash", hash);
}
};
异步插件
如果插件会被异步执行,那么应该使用下面的方式来完成:
applyPluginsAsync(name: string, args: any..., callback: (err?: Error) -> void)
此时插件处理函数调用的时候会传入 args 和签名为 (err?: Error) -> void 的回调函数。我们的处理函数将会按照注册时候的顺序被执行。而回调函数 callback() 将会在所有的处理函数被调用以后调用。这种模式常常用于如 "emit"、"run"等钩子函数。比如下面的 Compiler 的 run 方法的具体逻辑。
self.applyPluginsAsync("run", self, function(err) {
if(err) return callback(err);
self.readRecords(function(err) {
if(err) return callback(err);
//2.调用compile的回调函数
self.compile(function onCompiled(err, compilation) {
//其他代码逻辑
});
});
});
异步瀑布流插件
此时所有的插件将会被异步执行,同时遵循瀑布流的方式。此时以下面的方式来调用:
applyPluginsAsyncWaterfall(name: string, init: any, callback: (err: Error, result: any) -> void)
此时插件的回调函数在调用的时候传入当前的值,回调函数被调用的时候会有如下的签名 (err: Error, nextValue: any) -> void。如果回调函数被调用了,那么 nextValue 就会成为下一个处理函数的当前值。第一个处理函数的当前值为 init。当所有的处理函数都执行以后,回调函数会传入最后一个插件的返回值。如果任何一个处理函数传入了一个 err,那么回调函数将会传入错误参数 err,此时余下的所有的处理函数都不会被执行。这种模式常常用于如 "before-resolve" 或者 "after-resolve"。
Webpack 插件调用顺序
Webpack 的源码中经常会看到上面说的执行插件注册的方法,我们给出下面的 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);
});
});
});
});
}
而各个钩子函数执行的顺序可以查看下面的内容:
'before run'
'run'
compile:func//调用 compile() 函数
'before compile'
'compile'//(1)compiler 对象的第一阶段
newCompilation:object//创建 compilation 对象
'make' //(2)compiler 对象的第二阶段
compilation.finish:func
"finish-modules"
compilation.seal
"seal"
"optimize"
"optimize-modules-basic"
"optimize-modules-advanced"
"optimize-modules"
"after-optimize-modules"//首先是优化模块
"optimize-chunks-basic"
"optimize-chunks"//然后是优化 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"//首先优化 module-order,然后优化 module-id
"optimize-module-ids"
"after-optimize-module-ids"
"revive-chunks"
"optimize-chunk-order"
"before-chunk-ids"//首先优化 chunk-order,然后 chunk-id
"optimize-chunk-ids"
"after-optimize-chunk-ids"
"record-modules"//record module 然后 record chunk
"record-chunks"
"before-hash"
compilation.createHash//func
"chunk-hash"//webpack-md5-hash
"after-hash"
"record-hash"//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"//(4)完成模块构建和编译过程(seal 函数回调)
"emit"//(5)compile 函数的回调,compiler 开始输出 assets,是改变 assets 最后机会
"after-emit"//(6)文件产生完成
网友评论