第 9 阶段:搞懂、搞透前端构建第 4 天
上一节内容我们实现了一个 loader 从使用 loader 到实现 loader · webpack,这节内容我们讲讲 webpack 中另一个关键概念 plugin。plugin 就是一个插件,它的作用就是进行功能扩展。比如手机中的 App 其实也是一个插件,它基于手机已有的功能进行扩展;VSCode(第1天:开篇词,开发环境准备) 中强大的插件系统,也是基于 VSCode 已有的功能进行扩展。
同样, webapck 的插件系统也类似,当它对文件进行打包时会经过各种阶段,每一阶段发生时,它都会通知插件干了什么,插件本身可以获取到 webpack 整个执行环境的「方法和数据」,有了这些方法和数据,插件可以做它想要做的事情。
上面从宏观的角度分析了什么是插件,但真正的难点是实现一个插件,本文基于 webpack4.43.0 版本实现插件。我们实现一个 CleanPlugin,作用是打包之前清除先前打包的文件,也就是说每次打包时都会先把 dist(加入 output 为 dist) 目录中所有的文件删除。
1、创建一个项目 day4-plugin,目录结构如下:
index.html HTML 页面模板:
index.js 动态创建一个 p 标签,如果看不懂这个函数可以前往 让 JavaScript 文件代码相互独立:
2、其中 webpack.config.js 配置如下,我创建了两个插件,从配置中可以看到插件的本质是一个类,只需要把实例放到 plugins 这个数组中即可:
3、在 package.json 中配置 npm script:
4、在根目录下执行 npm run build 进行打包:
5、打包成功后打开 index.html 文件,动态创建的内容显示出来了:
整个项目是跑起来了,我们看看 plugin 的本质是什么,打开 plugins/cleanPlugin.js,发现红色小箭头指向的内容我们都不懂,没关系,这是我们今天要掌握的内容:
创建 plugin 需要下面几个步骤:
1、定义一个 JavaScript 类(class CleanPlugin)或者函数;
class ClearPlugin {
constructor(option) {
console.log('ClearPlugin constructor called');
}
}
2、需要定义一个 apply 方法;
当插件被安装到 webpack 时,会调用 plugin 实现的函数 apply,这个方法只会被调用一次,携带一个参数 compiler,你可以把它看做是 webpack 的实例,或者是 webpack 的的大管家,包含了 webpack 中所有的数据和方法。
class ClearPlugin {
constructor(option) {
console.log('ClearPlugin constructor called');
}
apply(compiler) {
//
}
}
有关 comilper 的源码,可以查看:
https://github.com/webpack/webpack/blob/master/lib/Compiler.js
有几个概念需要特别留意下,比如 hooks,通过这个属性,可以监听到在整个编译过程中 webpack 发出的事件,hook 是 webpack 设计的核心概念。我们打印一下 compiler.hooks,可以看到有非常多的事件可以 hook:
3、指定 hook 来「钩住」你想要的事件;
插件的核心是能够监听的 webpack 整个编译过程中产生事件,compiler 有一个属性 hooks,这里保存了所有能够被 hook 的事件:
我们来钩一个 beforeRun 的事件,beforeRun 其实是 AsyncSeriesHook 对象,关于这些 hook 对象我们将单独开一节内容来讲:
const hooks = compiler.hooks;
console.log(Object.keys(hooks));
compiler.hooks.beforeRun.tap('clean-plugin', () => {
console.log('webpack beforeRun');
});
4、处理 webpack 内部提供的数据;
官方提供了一个特别有意思的例子,在打包结果中添加一个编译文件,实现代码如下,需要注意一点这里使用的是 tapAsync:
hooks.emit.tapAsync('clean-plugin', (compilation, callback) => {
// Create a header string for the generated file:
var filelist = 'In this build:\n\n';
// Loop through all compiled assets,
// adding a new line item for each filename.
for (var filename in compilation.assets) {
filelist += '- ' + filename + '\n';
}
// Insert this list into the webpack build as a new file asset:
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
}
};
callback();
});
5、当处理的任务结束时,要给 webpack 一个回调告诉 webpack 我完事了;
回到我们的 CleanPlugin 中,我们的目的就是在 webpack 开始编译时把 output 目录下的文件清空。我这里借助了一个工具库 del,需要通过 npm install -S del 来安装这个库,然后用这个库对文件进行删除操作,具体实现如下:
const del = require("del");
// 1、定义一个 JavaScript 类(class CleanPlugin)或者函数;
class ClearPlugin {
constructor(option) {
console.log('ClearPlugin constructor called');
}
// 2、需要定义一个 apply 方法;
apply(compiler) {
const hooks = compiler.hooks;
if (!compiler.options.output || !compiler.options.output.path) {
// 没有配置输出文件,什么也不干
return;
}
this.outputPath = compiler.options.output.path;
// 3、指定 hook 来「钩住」你想要的事件;
hooks.emit.tapAsync('clean-plugin', (compilation, callback) => {
// 4、处理 webpack 内部提供的数据;
const stats = compilation.getStats();
if (stats.hasErrors()) {
// 有错误不能删除文件
return;
}
// 开始删除文件
(async () => {
await del([this.outputPath]);
// 5、当处理的任务结束时,要给 webpack 一个回调告诉 webpack 我完事了;
callback();
})();
});
}
}
实现完以后,我们在 dist 文件夹下随便创建几个文件,我创建了 suyan.js:
当执行 npm run build 时,suyan.js 这个文件将被删除。
到此,今天的内容就结束了,从使用 plugin 到自己实现一个 plugin。本节代码:https://github.com/lefex/FE/tree/master/webpack。
最后,诚挚邀请你来思考下面的问题并打卡:
1、平时工作中你用到了哪些 plugin,它们的作用是啥?
大家加油!
推荐阅读:
从使用 loader 到实现 loader · webpack
https://webpack.js.org/contribute/writing-a-plugin/#creating-a-plugin