目录
简而言之,plugin可以在webpack运行到某个时刻帮你做一些事情. plugin会在webpack初始化时,给相应的生命周期函数绑定监听事件,直至webpack执行到对应的那个生命周期函数,plugin绑定的事件就会触发.
不同的plugin定义了不同的功能,比如clean-webpack-plugin插件,它会在webpack重新打包前自动清空输出文件夹,它绑定的事件处于webpack生命周期中的emit.
再以下面代码使用的插件HtmlWebpackPlugin举例,它会在打包结束后根据配置的模板路径自动生成一个html文件,并把打包生成的js路径自动引入到这个html文件中.这样便刨去了单调的人工操作,提高了开发效率.
webpack将整个打包构建过程切割成了很多个环节,每一个环节对应着一个生命周期函数(简称钩子函数,也可称hook).
webpack官方文档记录的所有hook函数的数量达到上百个,我们抽取其中小部分的核心钩子作为学习素材.
观察下图,我们首先要对webpack的执行过程构建立一个宏观上的整体认知.

webpack包含两个很重要的基础概念,分别是compiler和compilation。
下面代码可以对compiler建立初步的认知:
代码头部首先引入webpack和配置文件参数options,通过执行webpack(options)即可生成compiler对象,再执行对象的run方法就能开始启动代码编译.
// compiler
const webpack = require("webpack");
const options = require("../webpack.config.js");
const compiler = webpack(options);
compiler.run(); // 启动代码编译
compilation实例主要负责代码的编译和构建,每进行一次代码的编译(例如日常开发时按ctrl + s保存修改后的代码),都会重新生成一个compilation实例负责本次的构建任务.
compilation下的钩子含义如下.
整体执行流程已经梳理了一遍,接下来深入到上图中标记的每一个钩子函数,理解其对应的时间节点.
compiler进入make阶段后,compilation实例被创建出来,它会先触发buildModule阶段定义的钩子,此时compilation实例依次进入每一个入口文件(entry),加载相应的loader对代码编译.
代码编译完成后,再将编译好的文件内容调用 acorn 解析生成AST语法树,按照此方法继续递归、重复执行该过程.
所有模块和和依赖分析完成后,compilation进入seal 阶段,对每个chunk进行整理,接下来进入optimize阶段,开启代码的优化和封装.
到这里,我们就明白了webpack基于插件的架构体系,编写的plugin就是在上面这些不同的时间节点里绑定一个事件监听函数,等到webpack执行到那里便触发函数。
假设我现在想在compiler的emit钩子下绑定几个监听函数,那么应该如何绑定,其次又如何确保绑定的函数到了相应的时间节点会触发?
这里涉及到了发布-订阅的事件机制,webpack内部借助了Tapable第三方库实现了事件的绑定和触发.
Tapable是一个用于事件发布订阅的第三方库,需要通过npm安装使用,它和Node.js中的EventEmitter类似.
webpack中的compiler和compilation都继承了Tapable,因此compiler和compilation才具备了事件绑定和触发事件的能力.
我们接下里直接通过代码快速学习Tapable的使用方式.
代码头部引入同步钩子函数SyncHook,分别绑定三个事件开始刷牙、正在洗脸和吃早餐.
- const { SyncHook } = require("tapable");
- const prepareHook = new SyncHook(["arg1","arg2"]); // 创建钩子,定义参数
- prepareHook.tap("brushTeeth",(arg)=>{ //绑定事件
- console.log(`开始刷牙:${arg}`)
- })
- prepareHook.tap("washFace",(arg)=>{ //绑定事件
- console.log(`正在洗脸:${arg}`)
- })
- prepareHook.tap("breakfast",(arg)=>{ //绑定事件
- console.log(`吃早餐:${arg}`)
- })
- prepareHook.call("准备阶段"); //触发事件
prepareHook.call("准备阶段")一执行就会触发上面绑定的三个事件,输出结果如下.
开始刷牙:准备阶段 正在洗脸:准备阶段 吃早餐:准备阶段
从上面案例可以看出,只要call命令一触发,SyncHook绑定的事件会按照定义的顺序依次执行.
有时候我们定义的事件不光只包含同步行为,它可能也存在发起ajax请求、文件上传下载这样的异步任务.
Tapable提供的AsyncSeriesHook钩子可以帮助我们定义异步任务.它绑定事件的回调函数的最后一个参数next,需要在当前异步任务执行完成后调用一下,如此才能进入下一个异步任务.
- const { AsyncSeriesHook } = require("tapable");
- const workHook = new AsyncSeriesHook(["arg1"]);
- workHook.tapAsync("openComputer",(arg,next)=>{ //绑定事件
- setTimeout(()=>{
- console.log(`打开电脑:${arg}`);
- next();
- },1000)
- })
- workHook.tapAsync("todoList",(arg,next)=>{ //绑定事件
- setTimeout(()=>{
- console.log(`列出日程安排:${arg}`);
- next();
- },1000) })
- workHook.tapAsync("processEmail",(arg,next)=>{ //绑定事件
- setTimeout(()=>{
- console.log(`处理邮件:${arg}`);
- next();
- },2000) })
- workHook.callAsync("工作阶段",()=>{ //触发事件
- console.log(`异步任务完成`) // 所有异步任务全部执行完毕,回调函数才会触发
- });
workHook.callAsync一执行便触发绑定的异步事件,输出结果如下:
打开电脑:工作阶段 列出日程安排:工作阶段 处理邮件:工作阶段 异步任务完成
打开电脑:工作阶段最先输出,过了1s后输出列出日程安排:工作阶段,再过2s输出处理邮件:工作阶段.最后输出异步任务完成.
上面代码分别使用同步钩子和异步钩子做演示,输出结果很容易理解.如果同一份代码同时定义了同步钩子和异步钩子,一起触发执行顺序如何呢?
经过测试,同步任务都执行完毕后才会执行异步任务队列.如果代码中定义了多个同步任务队列,一起触发执行顺序如何呢?
它们也会按照调用(call)顺序依次执行相应的队列任务,上一个队列任务都执行完了才会开始执行下一个任务队列.如果同一份代码定义多个异步任务队列,一起触发执行顺序如何呢?
异步任务队列并不会按照同步任务队列那样按照顺序先后执行,异步任务队列与异步任务队列之间会并行执行.
其实,plugin本质上是一个对外导出的class,类中包含一个固定方法名apply.
apply函数的第一个参数就是compiler,我们编写的插件逻辑就是在apply函数下面进行编写.
程序中已经获取了compiler参数,那我们就可以在compiler的各个钩子函数中绑定监听事件。
比如在emit阶段绑定一个监听事件,这代表主程序一旦执行到 emit 阶段,绑定的回调函数就会触发。此时主程序处于emit阶段时, compilation 已经将代码编译构建完了,下一步会将内容输出到文件系统。
此时 compilation.assets 存放着即将输出到文件系统的内容,如果这时候我们操作compilation.assets数据,势必会影响最终打包的结果。
所以我们使用新增属性的方式来定义,比如直接在compilation.assets上新增属性名copyright.txt,并定义好文件内容和长度。
这里需要引起注意,由于程序中使用tapAsync(异步序列)绑定监听事件,那么回调函数的最后一个参数会是next,异步任务执行完成后需要调用next,主程序才能进入到下一个任务队列.
最终打包后的目标文件夹下会多出一个copyright.txt文件,里面存放着字符串this is my copyright.
介绍完了插件的编写,插件的使用也同样简单.
首先在webpack配置文件引入插件,然后在plugins数组中new一下引入的插件,即完成了plugin的注入.此后webpack再执行打包,运行到了相应的事件节点就会执行plugin定义的监听函数.
下面看个完整的plugin插件开发和使用的例子:
- class CopyRightPlugin {
- apply(compiler){
- compiler.hooks.emit.tapAsync("CopyRightPlugin",(compilation,next)=>{
- setTimeout(()=>{
- // 模拟ajax获取版权信息
- compilation.assets['copyright.txt'] = {
- source:function(){
- return "this is my copyright"; // //文件内容
- },
- size:function(){
- return 20; // 文件大小
- }
- }
- next();
- },1000)
- })
- }
- }
- module.exports = CopyRightPlugin; //插件导出
首先在webpack配置文件 webpack.config.js 中引入插件, 然后在 plugins 定义的数组中new一下引入的插件,即完成了plugin的注入。此后webpack再执行打包,运行到了相应的事件节点就会执行plugin定义的监听函数。代码如下:
- const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
- const webpack = require('webpack'); // 访问内置的插件
- const path = require('path');
- module.exports = {
- entry: './src/index.js',
- output: {
- path: path.resolve(__dirname, 'dist'),
- },
- module: {
- rules: [
- {
- test: /.(js|jsx)$/,
- use: 'babel-loader',
- },
- ],
- },
- plugins: [
- new HtmlWebpackPlugin({ template: './src/index.html' }) // webpack执行打包,运行到相应的事件节点会执行plugin定义的监听函数
- ]
- };