您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~
前言
学习过Webpack都知道,Webpack在打包项目模块的过程中,有两个配置项是编译中的主角,分别是:
- Loader 模块预处理器
- Plugin 插件
Loader会在解析每一个模块前判断文件名是否在loader配置项中存在,如果存在,则会在打包前进行文件预处理,因为Webpack本身只支持js模块的打包。
而Plugin则会在整个打包过程中,以"恰当的时机",来进行插件的运行,常用于优化、运行服务、dist文件处理等等。那么问题来了,恰当的时机是什么时机?在接下来的内容中,你将了解,plugin是什么、plugin在打包构建过程中的原理、如何仿写一个plugin。
Tapable
Webpack是建立于插件系统之上的事件流工作系统,而插件系统的根基则是tapable这个库,tapable通过观察者模式实现了事件的监听和触发,提供给了Webpack很多Hook类,这些Hook类可以用来生成实例对象。
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParalleHook,
AsyncParalleBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesLoopHook,
AsyncSeriesWaterfallHook
} = require("tapable");
tapable提供了上述十大Hook类以及三种实例方法:tap、tapAsync和tapPromise,为Webpack整个工作创建了观察 -> 触发插件的工作流。
Webpack Plugin生命周期
webpack源码中对于plugin执行前有如下代码:
compiler = new Compiler(options.context);
compiler.options = options;
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
可以看到,如果在webpack.config.js中配置了plugins并且他是个数组,webpack会依次去执行每一个plugin,并且把compiler传给每一个plugin。
那么compiler到底是什么?顺着代码到compiler.js可以看到:
class Compiler extends Tapable {
}
class Compilation extends Tapable {
}
其实就是继承于Tapable的子类,那么Tapable的观察者模式就可以对上了,Compiler基于观察者模式,设计出了一系列的生命周期钩子函数。基于tapable的支持,Webpack插件系统中提供了大量的生命周期钩子函数,让每一个插件都可以在需要的时候去执行。
上图是Webpack在compiler在整个打包中所涉及到的生命周期,可以试想一下。
如clean-webpack-plugin插件在打包前,就应该清除目录,因此它应该会在done(解析完毕)执行,去清除我们的output目录;html-webpack-plugin插件会根据打包产出资源构建出一个web服务器,因此它应该会在所有钩子结束后,无任何error的情况下去执行该plugin,开启一个服务器。
手写一个plugin
我们在项目中创建一个HelloPlugin.js文件,在其中写入:
class HelloPlugin {
constructor(options) {
console.log(options);
}
apply(compiler) {
compiler.hooks.done.tap('HelloPlugin', () => {
console.log('HelloPlugin');
}
}
}
并在webpack.config.js中定义:
const HelloPlugin = require('./HelloPlugin.js');
module.exports = {
...
plugins: [
new HelloPlugin(),
],
...
}
执行npx webpack,可以看到打印结果。
在Webpack官方推荐:使用es6 class去定义每一个plugin,使用new实例化出一个构造函数后,可以再constructor获取到对应的传参。并且apply方法会在插件初始化时被调用一次,内部方法就是插件的功能,而compiler.hooks后可以选择插件在某一个生命周期时去执行,并且可以在class中声明多个这样的钩子函数。
钩子函数的第一个参数为插件名,第二个参数是回调函数,在回调函数中可以获取Compilation对象。
compiler和compilation都存放着编译相关的信息,compiler存放的是webpack全局环境信息,而compilation存放的是开发模式运行时一次性、不间断编译的信息。
模拟copy-webpack-plugin
copy-webpack-plugin可以将项目目录中某一个文件夹或文件直接复制到打包目标文件夹中,这里我们实现一下。
创建一个CopyPlugin.js文件,代码如下:
const fs = require('fs');
class CopyPlugin {
constructor(options) {
this.from = options.from;
this.to = options.to;
}
apply(compiler) {
const {
from, to } = this;
const isDir = fs.statSync(from).isFile() ? false : true;
compiler.hooks.done.tap('CopyPlugin', () => {
fs.cp(from, to, {
recursive: isDir }, (err) => {
if (err) {
throw err;
}
})
})
}
}
module.exports = CopyPlugin;
在webpack.config.js中配置:
const path = require('path');
const CopyPlugin = require('./copyPlugin');
module.exports = {
...
plugins: [
new CopyPlugin({
from: path.resolve(__dirname, 'static'),
to: path.resolve(__dirname, 'dist', 'static')
})
],
...
}
这里我们选择将static文件夹复制到dist目录中,使用方式和原来的一样,执行npx webpack可以看到:
具体逻辑就是复制一个文件夹或文件,执行时间这里是选择在打包结束后进行复制的,对应生命周期compiler.hooks.done
模拟clean-webpack-plugin
clean-webpack-plugin会在打包出新的文件前清空整个打包目录,所以应该在生命周期偏起始的时候去执行,这里我们选择compiler.hooks.make。
创建CleanPlugin.js,写入代码:
const fs = require('fs');
const path = require('path')
class CleanPlugin {
apply(compiler) {
compiler.hooks.make.tap('CleanPlugin', () => {
function clearDir(dirPath) {
let files = []
if (fs.existsSync(dirPath)) {
files = fs.readdirSync(dirPath)
files.forEach(f => {
const absPath = dirPath + '/' + f;
if (fs.statSync(absPath).isDirectory()) {
clearDir(absPath);
} else {
fs.unlinkSync(absPath);
}
})
}
}
clearDir(path.resolve(__dirname, 'dist'))
})
}
}
module.exports = CleanPlugin;
在webpack.config.js中配置:
const path = require('path');
const CleanPlugin = require('./CleanPlugin');
module.exports = {
...
plugins: [
new CleanPlugin(),
],
...
}
运行npx webpack,会发现,在打包中,目录被清除了,最后又出现文件了。
总结
最后结合了两个案例的仿写,相信你已经对Webpack plugin的组成和运行原理有了一定的理解,它其实就如同Vue组件的生命周期一样,Webpack在打包过程中也是会有这样的生命周期去执行一系列的插件,如果觉得文章对你有帮助,请点个赞吧~