目录
前言
- 介绍:plugin(插件)是webpack的几大要素之一,plugin的目的在于在webpack构建打包生命周期中中执行一些可扩展性功能。
- 原理:
- plugin使用方式:通常我们会先通过npm安装到本地,然后在配置文件(webpack.config.js)的头部引入,在plugins那一栏使用new关键字生成插件的实例注入到webpack,webpack注入了plugin之后,那么在webpack后续构建的某个时间节点就会触发plugin定义的功能
- plugin一旦注入到webpack中后,它会在对应的生命周期函数里绑定一个事件函数,当webpack的主程序执行到那个生命周期对应的处理工序时,plugin绑定的事件就会触发.
简而言之,plugin可以在webpack运行到某个时刻帮你做一些事情. plugin会在webpack初始化时,给相应的生命周期函数绑定监听事件,直至webpack执行到对应的那个生命周期函数,plugin绑定的事件就会触发.
不同的plugin定义了不同的功能,比如clean-webpack-plugin插件,它会在webpack重新打包前自动清空输出文件夹,它绑定的事件处于webpack生命周期中的emit.
再以下面代码使用的插件HtmlWebpackPlugin举例,它会在打包结束后根据配置的模板路径自动生成一个html文件,并把打包生成的js路径自动引入到这个html文件中.这样便刨去了单调的人工操作,提高了开发效率.
webpack 钩子函数
webpack将整个打包构建过程切割成了很多个环节,每一个环节对应着一个生命周期函数(简称钩子函数,也可称hook).
webpack官方文档记录的所有hook函数的数量达到上百个,我们抽取其中小部分的核心钩子作为学习素材.
观察下图,我们首先要对webpack的执行过程构建立一个宏观上的整体认知.
webpack compiler和compilation
webpack包含两个很重要的基础概念,分别是compiler和compilation。
- compiler是webpack的执行器,控制着程序的执行,有6个钩子函数,compiler会从左到右执行依次执行每一个钩子定义的监听事件队列。
compiler怎么来的呢?
下面代码可以对compiler建立初步的认知:
代码头部首先引入webpack和配置文件参数options,通过执行webpack(options)即可生成compiler对象,再执行对象的run方法就能开始启动代码编译.
// compiler const webpack = require("webpack"); const options = require("../webpack.config.js"); const compiler = webpack(options); compiler.run(); // 启动代码编译
- 上图中,当compiler执行make阶段时,标志着代码的编译工作正式开始,这时候会创建compilation对象完成相关任务。
- compilation会依次执行第二行的3个钩子,等到代码的编译工作结束后,主线程又回到了compiler,继续往下执行emit钩子.
- 简而言之,compiler执行到make和emit之间时,compilation对象便出场了,它会依次执行它定义的一系列钩子函数,像代码的编译、依赖分析、优化、封装正是在这个阶段完成的
compilation实例
compilation实例主要负责代码的编译和构建,每进行一次代码的编译(例如日常开发时按ctrl + s保存修改后的代码),都会重新生成一个compilation实例负责本次的构建任务.
compilation下的钩子含义如下.
- buildModule: 在模块构建开始之前触发,这个钩子下可以用来修改模块的参数
- seal: 构建工作完成了,compilation对象停止接收新的模块时触发
- optimize: 优化阶段开始时触发
钩子执行步骤
整体执行流程已经梳理了一遍,接下来深入到上图中标记的每一个钩子函数,理解其对应的时间节点.
- entryOption:webpack开始读取配置文件的Entries,递归遍历所有的入口文件.
- run: 程序即将进入构建环节
- compile: 程序即将创建compilation实例对象
- make:compilation实例启动对代码的编译和构建
- emit: 所有打包生成的文件内容已经在内存中按照相应的数据结构处理完毕,下一步会将文件内容输出到文件系统,emit钩子会在生成文件之前执行(通常想操作打包后的文件可以在emit阶段编写plugin实现).
- done: 编译后的文件已经输出到目标目录,整体代码的构建工作结束时触发
compiler进入make阶段后,compilation实例被创建出来,它会先触发buildModule阶段定义的钩子,此时compilation实例依次进入每一个入口文件(entry),加载相应的loader对代码编译.
代码编译完成后,再将编译好的文件内容调用 acorn 解析生成AST语法树,按照此方法继续递归、重复执行该过程.
所有模块和和依赖分析完成后,compilation进入seal 阶段,对每个chunk进行整理,接下来进入optimize阶段,开启代码的优化和封装.
到这里,我们就明白了webpack基于插件的架构体系,编写的plugin就是在上面这些不同的时间节点里绑定一个事件监听函数,等到webpack执行到那里便触发函数。
假设我现在想在compiler的emit钩子下绑定几个监听函数,那么应该如何绑定,其次又如何确保绑定的函数到了相应的时间节点会触发?
这里涉及到了发布-订阅的事件机制,webpack内部借助了Tapable第三方库实现了事件的绑定和触发.
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:
其实,plugin本质上是一个对外导出的class,类中包含一个固定方法名apply.
apply函数的第一个参数就是compiler,我们编写的插件逻辑就是在apply函数下面进行编写.
第二步编写plugin:
程序中已经获取了compiler参数,那我们就可以在compiler的各个钩子函数中绑定监听事件。
比如在emit阶段绑定一个监听事件,这代表主程序一旦执行到 emit 阶段,绑定的回调函数就会触发。此时主程序处于emit阶段时, compilation 已经将代码编译构建完了,下一步会将内容输出到文件系统。
第三步 处理逻辑:
此时 compilation.assets 存放着即将输出到文件系统的内容,如果这时候我们操作compilation.assets数据,势必会影响最终打包的结果。
所以我们使用新增属性的方式来定义,比如直接在compilation.assets上新增属性名copyright.txt,并定义好文件内容和长度。
这里需要引起注意,由于程序中使用tapAsync(异步序列)绑定监听事件,那么回调函数的最后一个参数会是next,异步任务执行完成后需要调用next,主程序才能进入到下一个任务队列.
最终打包后的目标文件夹下会多出一个copyright.txt文件,里面存放着字符串this is my copyright.
plugin插件使用
介绍完了插件的编写,插件的使用也同样简单.
首先在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定义的监听函数
]
};