使用webpack开发一些自定义功能的插件,最主要的目的是了解webpack工作流的方式,更深层次的认识和了解webpack工作原理,这样以后如有需要,也可结合自身所在业务去定制不同的插件,来提高开发效率。
在webpack整个工作流中在不同的阶段对外提供了不同的hook,使得开发者可以利用这些钩子做一些自己定制化功能的plugin。比如在插件初始化之后会触发 afterPlugins hook,在执行之前回触发beforeRun hook,在编译之前会触发beforeCompile hook 编译的过程会触发 compile hook…等等,简言之就是webpack在不同的工作阶段会对外抛出不同的事件钩子,我们可以在抛出的这些事件钩子中去处理一些定制化的事情来满足我们的业务。
其实webpack的钩子提供是高度依赖和扩展了 tapable这个npm 包。大类上可以分为Compiler钩子和compilation钩子。
对开发者而言要去开发一个自定义插件的话,需要的是 Compiler这种类型的钩子,我们可以在这上面注册和调用我们的插件。比如我们在所有模块打包完之后再添加一些需要打包的文件可以在 emit 做修改。这个钩子的注册其实是在webpack/lib/Compiler.js文件中的 constructor 构造函数中,如下所示:
在上述提供的钩子中可以使用同步的方式tap(即触及)也可以使用异步的方式运行。如下
class DonePlugin{
apply(compiler){
compiler.hooks.done.tap('DonePlugin', (stats)=>{
console.log('同步编译完成~~~');
});
}
}
class AsyncPlugin{
apply(compiler){
compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, cb)=>{
setTimeout(()=>{
console.log('等待3s再发射文件到指定文件夹');
cb();
}, 3000)
})
}
}
class HelloAsyncPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise('HelloAsyncPlugin', compilation => {
return new Promise((resolve, reject) => {
setTimeout(function() {
console.log('异步工作完成……');
resolve();
}, 1000);
});
});
}
}
一般一个具体的plugin 由下面部分组成:
所以自定义插件中一般都会有 apply 方法,这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象,hooks 就在compiler对象中。根据第三点我们 tap(触及)钩子的方式不同分为同步异步,异步又可以有两种方式。
新建一个webpack 工程,里面随便加几个文件,然后配置好webpack 的基本配置,基于此之上我们来写一个自定义插件,先在工程目录下新建一个plugin文件夹用于存储我们自定义的插件。
我们使用webpack暴露出来的done这个hook来在整个编译完成的时候在控制台输出一行信息通知用户当前webpack打包已近完成的信息,文件的名称我们就称之为 DonePlugin表完成的插件
DonePlugin.js 代码如下:
// 自定义 webpack plugin.
class DonePlugin{
constructor(params){
}
apply(compiler){
compiler.hooks.done.tap('DonePlugin', (stats)=>{
/console.log('当前webpack同步编译完成~~~');
});
}
}
module.exports = DonePlugin;
在webpack.config.js中引入这个模块并添加到plugin配置数组中。
// 导入自定义插件
const DonePlugin = require('./plugins/DonePlugin');
// webpack 配置中添加自定义插件.
module.exports = {
plugins:[
new DonePlugin(),
],
}
之后我们使用 npx webpack 查看一下效果(npx是npm的一个工具,会自动查找当前依赖包中的可执行文件,需要npm>5.2版本)如下:
这里为了看得清楚,使用了chalk加了个颜色。
我们使用 emit 这个hook 在webpack打包完成之后准备将文件发射到dist文件夹之前我们延时5秒再发射。
这个插件命名为AsyncPlugin,其文件代码如下:
const chalk = require('chalk');
class AsyncPlugin{
apply(compiler){
// console.log('自定义异步组件');
compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, cb)=>{
setTimeout(()=>{
console.log(chalk.blue.bgRed.bold('等待5s再发射文件'));
cb();
}, 5000)
})
}
}
module.exports = AsyncPlugin;
在webpack.config.js配置上述插件:
const AsyncPlugin = require('./plugins/AsyncPlugin');
module.exports = {
plugins:[
new AsyncPlugin(),
],
}
看一下加了延时发射文件之后的效果:
可以看到添加了这个延时插件之后需要等待5s之后才会把文件发射到dist文件夹之下,在webpack.config.js 配置文件去除这个plugin之后立马就可以发射。
在plugin文件夹下新建 TapPromisePlugin.js 文件,在该文件中输入代码如下:
const chalk = require('chalk');
class TapPromisePlugin{
apply(compiler){
// console.log('自定义异步组件');
compiler.hooks.emit.tapPromise('TapPromisePlugin', compilation =>{
// 返回一个 Promise,在我们的异步任务完成时 resolve……
return new Promise((resolve, reject)=>{
setTimeout(() => {
console.log(chalk.blue.bgRed.bold('使用 tapPromise 方式延时5s发射文件'))
resolve();
}, 5000);
})
})
}
}
module.exports = TapPromisePlugin;
在webpack.config.js配置上述插件
const TapPromisePlugin = require('./plugins/TapPromisePlugin');
module.exports = {
plugins:[
new TapPromisePlugin(),
],
}
效果如下:
可以看到实现的效果和上面使用callback的方式是一样的,都是webpack打包完成之后等待5s再发射文件到dist文件夹之下。
经过上面的了解,已近知道webpack 是如何工作的,上面写的自定义插件都很小,这里就演示开发一个具体的自定义插件功能,该插件可以在webpack打包完成之后生成一个markdown文件,来展示dist文件夹下一共有哪些文件,以及文件名和文件大小。插件的整体套路和上面差不多。
在执行完成编译的封存阶段(seal)之后,编译(compilation)的所有结构都可以遍历,而在 compilation 之中就包含着 webpack 工作流最后的输出结果,比如打包之后的所有文件都在 compilation.assets至上。大概长这个样子:
整个是一个对象,文件名为key, 文件的信息为value。
我们要做的就是修改这个对象,在其中添加一个markdown文件来显示dist文件下所有的打包文件。
在plugin文件夹下新建 FileListPlugin.js 文件。该文件代码如下:
class FileListPlugin{
constructor({filename}){
// 传入的参数挂载在这个类的实例上.
this.filename = filename;
}
apply(compiler){
compiler.hooks.emit.tap('FileListPlugin', (compilation)=>{
// compilation 是webpack 工作流中抛出来的内容,很多东西在这里,要修改工作流就修改这个即可
let assets = compilation.assets;
let content = `## 文件名 资源大小 \r\n`;
// 遍历打包之后的文件列表
Object.entries(assets).forEach(([filename, stateObj])=>{
content += `- ${filename} ${stateObj.size()}\r\n`
})
// 每个文件都有 source (该文件内容) 和 size 该文件大小
assets[this.filename] = {
source(){
return content
},
size(){
return content.length
}
}
})
}
}
module.exports = FileListPlugin;
在webpack.config.js 中引入并配置上述插件
const FileListPlugin = require('./plugins/FileListPlugin');
module.exports = {
plugins:[
new FileListPlugin({
filename:'list.md'
}),
],
}
该插件接受用户传入的参数作为文件名。配置之后运行效果如下:
也可以在插件运行完之后再次打印出assets中的信息发现多出了一个list.md。
这里展示的自定义插件都很简单,目的是为了更加深入的了解webpack的运行机制。不管开发出来的plugin功能多么牛逼,其基本的运行方式和plugin的骨架是相似的,webpack 就像是一个工作的引擎,我们可以基于此开发出功能更加强大和复杂的插件来扩充webpack的功能。
点我查看线上代码