说到webpack,我认识它好久了,差不多4年的时间,可是它不认识我,这就很尴尬。手写一个自定义的webpack插件,这个思想似乎存在很久了,之前总是断断续续的,直到今天,满打满算,应该是毕业正好3年,我来把这一块的东西给弥补上了。
特别强调:这篇文章只面向初级以下的人,其他段位的大神可自行划走,当然如果你想指点一二,评论区也是有你位置的。
话不多说,直接进入正题。
webpack浅认识
首先我们来回顾一下,webpack plugin是如何使用的:
javascript
// webpack配置如下
let HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
main: path.join(__dirname, 'src/index.js')
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html')
})
]
}
从上面我们可以看出,plugins是一个实例化类型的数组,意味着如果我们要写一个自定义的webpack plugin,那么我们导出来的插件应该是一个 function
或者是 class
。因为在js里,只有这2个关键字支持跟new
一块使用。
我们再回到webpack这个工具本身,我们知道它有非常多的plugin、loader,这些东西都是webpack团队开发的吗?
答案肯定不是,如果跟webpack相关的东西都是这个团队开发的,那岂不是要累死。因此为了让plugin、loader对于webpack来说可插拔,可扩展,这个团队基于tapable
实现了这样的一个流程控制。
tapable浅认识
大家可以翻阅一下跟webpack相关的一些源码,在这些源码里,我们会发现大量的类似下面的代码:
javascript
compiler.hooks.emit.tap('xxx', () => {});
compiler.hooks.done.tapAsync('xxx', () => {});
xxx.tap
等这类写法很符合tapable风格,我们再来看看看下面的代码:
javascript
let { SyncHook } = require('tapable');
class MyHook{
constructor(){
this.hooks = new SyncHook();
}
// 注册事件
registryEvent(eventName, eventFn){
this.hooks.tap(eventName, () => {
eventFn();
});
}
// 执行事件
executeEvent(){
this.hooks.call();
}
}
let compiler = new MyHook();
compiler.registryEvent('tapable-1', () => {
console.log('执行111');
});
complier.registryEvent('tapable-2', () => {
console.log('执行222');
})
complier.executeEvent();
我们再执行一下这个代码,控制台里面输出了我们打印的信息。
上面的代码非常简单,就是一个 注册事件、触发事件
这样的流程,只不过我们是基于tapable
第三方库实现的。
有了对"tapable"这样的认识,再回到webpack上,我们知道webpack的运行是分很多个阶段的,比如初始化阶段(initialize)
、编译阶段(compilation)
、产出阶段(emit)
、完成阶段(done)
。
我上面说的这几个阶段只是大致的框架,每个框架下面还会细分为很多个小阶段,比如"产出阶段"就分为"静态资源文件生成到指定目录前(emit)"、"静态资源文件生成到指定目录后(afterEmit)"。
每个阶段都是一个tapable对象,这点在webpack源码里就有直接的体现:
每个阶段在执行前以及执行后,都会去调用new SyncHook().call()
这样类似的方法,因此我们写webpack plugin的时候,我们需要知道自己的插件是在哪个阶段运行的,这点尤为重要。知道了在哪个阶段运行,那我们就可以在指定的阶段去注册相应的事件。
假如,我现在写一个插件,这个插件的运行时机是在webpack编译完成之后运行,我也不管它编译成功还是失败,根据上图的指示,我们知道,首先要拿到webpack编译对象,然后呢还要拿到编译对象的hooks对象,最后再拿到done对象,在done对象上去注册一个随机事件。当webpack编译结束后,我们的随机事件就会被触发了,代码可能就像下面这样:
javascript
compiler.hooks.done.tap('xxx', () => {
console.log('webpack编译完成');
});
那么问题来了,我们该如何拿到compiler.hooks.done
对象呢?
这一点从webpack源码里能够找到答案:
上面的图片告诉我们,如果我们的配置文件里,配置了plugins选项,并且plugins是一个数组,那么就会遍历数组的每一项,并且将compiler对象传入到每一项的apply方法里。
因此当我们在写自定义webpack plugin的时候,我们需要在插件上实现apply
方法,然后在apply方法里,我们就能够获取到compiler.hooks
对象,我们就可以顺理成章的在指定的阶段去添加监听事件,以此来完成特定的需求。
手写webpack插件
有了上面的基础认知,我们现在来完成一个插件的编写。从上面的信息我们得知,完成一个插件,必须要有3个步骤:
- 插件必须是一个
function
或者class对象
。 - 插件必须要实现apply方法。
- 最后将这个插件导出,以供webpack使用。
我们来准备下工程,工程目录如下:
javascript
| - project
| - node_modules
| - CustomWebpackPlugin
| - FileListPlugin.js
| - src
| - index.js
| - webpack.config.js
| - package.json
| - package.lock.json
我们来安装一下必要的依赖:
javascript
// 1、新建project目录
mkdir project
// 2、在这个目录下进行初始化的操作
npm init -y
// 3、安装必要依赖
npm install webpack --save-dev
配置webpack.config.js文件
javascript
let webpack = require('webpack');
let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');
let FileListPlugin = require('./CustomWebpackPlugin/FileListPlugin');
module.exports = {
mode: 'production',
entry: {
index: path.join(__dirname, 'src/index.js')
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
clean: true
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html')
}),
new FileListPlugin('readme.md')
]
}
添加项目启动命令(修改package.json文件):
json
// 其余字段都不变
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack --config webpack.config.js"
},
接下来就是编写FileListPlugin插件,代码如下:
javascript
class FileListPlugin {
constructor(fileName1){
this.fileName = fileName1;
}
apply(compiler){
let self = this;
const { webpack } = compiler;
// Compilation 对象提供了对一些有用常量的访问。
const { Compilation } = webpack;
// RawSource 是其中一种 "源码"("sources") 类型,
// 用来在 compilation 中表示资源的源码
const { RawSource } = webpack.sources;
compiler.hooks.thisCompilation.tap('fileListDone', (compilation) => {
compilation.hooks.processAssets.tap(
{
name: self.fileName,
// 用某个靠后的资源处理阶段,
// 确保所有资源已被插件添加到 compilation
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
// "assets" 是一个包含 compilation 中所有资源(assets)的对象。
// 该对象的键是资源的路径,
// 值是文件的源码
// 生成 Markdown 文件的内容
const content = '# 这是一级标题';
// 向 compilation 添加新的资源,
// 这样 webpack 就会自动生成并输出到 output 目录
compilation.emitAsset(
self.fileName,
new RawSource(content)
);
}
)
});
}
}
module.exports = FileListPlugin;
上面代码是webpack5官方给出的例子,webpack4当时给出的例子可不是这样。上面出现了2个钩子,一个是thisCompilation
,它代表着开始编译的阶段;另一个是processAssets
,它代表着所有的静态资源已经生成完毕。
这么多钩子函数我们是否都要记住?答案肯定是否的,但是你要熟悉这些钩子函数,只有熟悉了他们,你才能够让自己的插件正常的运行。
当我们运行项目的时候,就会发现在dist目录里能够再生成一个readme文件,这个是符合预期的。
最后
好啦小伙伴,本期分享到这里就结束啦,希望我的分享能够对你有帮助,我们下期再见啦,拜拜~~