webpack之plugin详解
基本概念
plugin(插件)是webpack的⽀柱功能,webpack整体的程序架构也是基于插件系统之上搭建的,plugin的⽬的在于解决loader⽆法实现的其他功能.
plugin使⽤⽅式如下⾯代码.通常我们需要集成某款plugin时,会先通过npm安装到本地,然后在配置⽂件(fig.js)的头部引⼊,
在plugins那⼀栏使⽤new关键字⽣成插件的实例注⼊到webpack.
webpack注⼊了plugin之后,那么在webpack后续构建的某个时间节点就会触发plugin定义的功能.
狭义上理解,webpack完整的打包构建流程被切割成了流⽔线上的⼀道道⼯序,第⼀道⼯序处理完,马上进⼊第⼆道⼯序,依此类推直⾄完成所有的⼯序操作.
每⼀道⼯序相当于⼀个⽣命周期函数,plugin⼀旦注⼊到webpack中后,它会在对应的⽣命周期函数⾥绑定⼀个事件函数,当webpack的主程序执⾏到那个⽣命周期对应的处理⼯序时,plugin绑定的事件就会触发.
简⽽⾔之,plugin可以在webpack运⾏到某个时刻帮你做⼀些事情. plugin会在webpack初始化时,给相应的⽣命周期函数绑定监听事件,直
⾄webpack执⾏到对应的那个⽣命周期函数,plugin绑定的事件就会触发.
不同的plugin定义了不同的功能,⽐如clean-webpack-plugin插件,它会在webpack重新打包前⾃动清空输出⽂件夹,它绑定的事件处
于webpack⽣命周期中的emit.
再以下⾯代码使⽤的插件HtmlWebpackPlugin举例,它会在打包结束后根据配置的模板路径⾃动⽣成⼀个html⽂件,并把打包⽣成的js路径⾃动引⼊到这个html⽂件中.这样便刨去了单调的⼈⼯操作,提⾼了开发效率.
// fig.js
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 访问内置的插件
const path = require('path');
entry: './src/index.js',
output: {
path: solve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /.(js|jsx)$/,
use: 'babel-loader',
},
]
,
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }) //
]
};
webpack程序架构
上⼀⼩结我们知道了webpack将整个打包构建过程切割成了很多个环节,每⼀个环节对应着⼀个⽣命周期函数(简称钩⼦函数,也可称hook).
webpack官⽅⽂档记录的所有hook函数的数量达到上百个,我们抽取其中⼩部分的核⼼钩⼦作为学习素材.
观察下图,我们⾸先要对webpack的执⾏过程构建⽴⼀个宏观上的整体认知.
webpack包含两个很重要的基础概念,分别是compiler和compilation,下⾯⼀⼀讲解.
compiler是webpack的⽀柱引擎,它相当于系统中枢,控制着程序的执⾏.
上图第⼀⾏有6个钩⼦函数(为⽅便讲解,中间环节省略了很多其他钩⼦),compiler会从左到右执⾏依次执⾏每⼀个钩⼦定义的监听事件队列.⽂字描述仍然有些⽣硬,通过阅读下⾯代码可以对compiler建⽴初步的认知.
代码头部⾸先引⼊webpack和配置⽂件参数options,通过执⾏webpack(options)即可⽣成compiler对象,再执⾏对象的run⽅法就能开始启动代码编译.
const webpack = require("webpack");
const options = require("../fig.js");
const compiler = webpack(options);
compiler.run(); // 启动代码编译
上图中,当compiler执⾏make阶段时,标志着代码的编译⼯作正式开始,这时候会创建compilation对象完成相关任务.
compilation会依次执⾏第⼆⾏标记的3个钩⼦,等到代码的编译⼯作结束后,主线程⼜回到了compiler,继续往下执⾏emit钩⼦.
简⽽⾔之,compiler执⾏到make和emit之间时,compilation对象便出场了,它会依次执⾏它定义的⼀系列钩⼦函数,像代码的编译、依赖分析、优化、封装正是在这个阶段完成的.
compilation实例主要负责代码的编译和构建.每进⾏⼀次代码的编译(例如⽇常开发时按ctrl + s保存修改后的代码),都会重新⽣成⼀
个compilation实例负责本次的构建任务.
整体执⾏流程已经梳理了⼀遍,接下来深⼊到上图中标记的每⼀个钩⼦函数,理解其对应的时间节点.
entryOption:webpack开始读取配置⽂件的Entries,递归遍历所有的⼊⼝⽂件.
run: 程序即将进⼊构建环节
compile: 程序即将创建compilation实例对象
make:compilation实例启动对代码的编译和构建
emit: 所有打包⽣成的⽂件内容已经在内存中按照相应的数据结构处理完毕,下⼀步会将⽂件内容输出到⽂件系统,emit钩⼦会在⽣成⽂件之前执⾏(通常想操作打包后的⽂件可以在emit阶段编写plugin实现).
done: 编译后的⽂件已经输出到⽬标⽬录,整体代码的构建⼯作结束时触发
compilation下的钩⼦含义如下.
buildModule: 在模块构建开始之前触发,这个钩⼦下可以⽤来修改模块的参数
seal: 构建⼯作完成了,compilation对象停⽌接收新的模块时触发
optimize: 优化阶段开始时触发
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("准备阶段")⼀执⾏就会触发上⾯绑定的三个事件,输出结果如下.
webpack打包流程 面试开始刷⽛:准备阶段
正在洗脸:准备阶段
吃早餐:准备阶段
从上⾯案例可以看出,只要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的开发流程就很容易理解了.⾸先创建⼀个js⽂件,输⼊下⾯代码.
plugin本质上是⼀个对外导出的class,类中包含⼀个固定⽅法名apply.
apply函数的第⼀个参数就是compiler,我们编写的插件逻辑就是在apply函数下⾯进⾏编写.
既然程序中已经获取了compiler参数,理论上我们就可以在compiler的各个钩⼦函数中绑定监听事件.⽐如下⾯代码会在emit阶段绑定⼀个监听事件.
主程序⼀旦执⾏到emit阶段,绑定的回调函数就会触发.通过上⾯的介绍可知,主程序处于emit阶段时,compilation已经将代码编译构建完了,下⼀步会将内容输出到⽂件系统.
此时compilation.assets存放着即将输出到⽂件系统的内容,如果这时候我们操作compilation.assets数据,势必会影响最终打包的结果.
下⾯代码直接在compilation.assets上新增⼀个属性名,并定义好了⽂件内容和长度.
这⾥需要引起注意,由于程序中使⽤tapAsync(异步序列)绑定监听事件,那么回调函数的最后⼀个参数会是next,异步任务执⾏完成后需要调⽤next,主程序才能进⼊到下⼀个任务队列.
最终打包后的⽬标⽂件夹下会多出⼀个⽂件,⾥⾯存放着字符串this is my copyright.
介绍完了插件的编写,插件的使⽤也同样简单.
⾸先在webpack配置⽂件引⼊插件,然后在plugins数组中new⼀下引⼊的插件,即完成了plugin的注⼊.此后webpack再执⾏打包,运⾏到了相应的事件节点就会执⾏plugin定义的监听函数.
//copyRight.js
class CopyRightPlugin {
apply(compiler){
it.tapAsync("CopyRightPlugin",(compilation,next)=>{
setTimeout(()=>{ // 模拟ajax获取版权信息
compilation.assets[''] = {
source:function(){
return "this is my copyright"; // //⽂件内容
},
size:function(){
return 20; // ⽂件⼤⼩
}
}
next();
},1000)
})
}
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论