一. 概念
在编译的整个生命周期中,Webpack
会触发许多事件钩子,Plugin
可以监听这些事件,根据需求在相应的时间点对打包内容进行定向的修改。
一个简单的 plugin 是这样的:
js
class Plugin{
// 注册插件时,会调用 apply 方法
// apply 方法接收 compiler 对象
// 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作
apply(compiler){
// compilation 是监听每次编译循环
// 每次文件变化,都会生成新的 compilation 对象并触发该事件
compiler.plugin('compilation',function(compilation) {})
}
}
注册插件:
js
// webpack.config.js
module.export = {
plugins:[
new Plugin(options),
]
}
二. 事件流机制
Webpack 就像工厂中的一条产品流水线。原材料经过 Loader 与 Plugin 的一道道处理,最后输出结果。
- 通过链式调用,按顺序串起一个个
Loader
; - 通过事件流机制,让
Plugin
可以插入到整个生产过程中的每个步骤中;
Webpack 事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播:
js
const { SyncHook } = require("tapable")
const hook = new SyncHook(['arg'])
// 订阅
hook.tap('event', (arg) => {
// 'event-hook'
console.log(arg)
})
// 广播
hook.call('event-hook')
Webpack
中两个最重要的类Compiler
与Compilation
便是继承于Tapable
,也拥有这样的事件流机制。
-
Compiler : 可以简单的理解为
Webpack
实例,它包含了当前Webpack
中的所有配置信息,如options
,loaders
,plugins
等信息,全局唯一,只在启动时完成初始化创建,随着生命周期逐一传递; -
Compilation : 可以称为 编译实例。当监听到文件发生改变时,
Webpack
会创建一个新的Compilation
对象,开始一次新的编译。它包含了当前的输入资源,输出资源,变化的文件等,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子; -
区别:
Compiler
全局唯一,且从启动生存到结束;Compilation
对应每次编译,每轮编译循环均会重新创建;
三. 常用 Plugin:
- UglifyJsPlugin: 压缩、混淆代码;
- CommonsChunkPlugin: 代码分割;
- ProvidePlugin: 自动加载模块;
- html-webpack-plugin: 自动创建一个HTML文件,并把打包好的JS插入到HTML文件中
- extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件; DefinePlugin: 定义全局变量;
- optimize-css-assets-webpack-plugin: CSS 代码去重;
- webpack-bundle-analyzer: 代码分析;
- compression-webpack-plugin: 使用 gzip 压缩 js 和 css;
- happypack: 使用多进程,加速代码构建;
- EnvironmentPlugin: 定义环境变量;
clean-webpack-plugin
在每一次打包之前,删除整个输出文件夹下所有的内容mini-css-extrcat-plugin
抽离CSS代码,放到一个单独的文件中optimize-css-assets-plugin
压缩css
一. tapable库中的钩子
js
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
同步
- SyncHook
同步hooks,任务回依次执行
js
const { SyncHook, SyncBailHook } = require('tapable')
class Lesson {
constructor(){
// 初始化hooks容器
this.hooks = {
go: new SyncHook(['address'])
}
}
tap(){
// 往hooks容器中注册事件/添加回调函数
this.hooks.go.tap('class1', address=>{
console.log('class1', address)
return address
})
this.hooks.go.tap('class2', address=>{
console.log('class2', address)
})
}
start(){
// 触发hooks
this.hooks.go.call('打印SyncHook')
}
}
const item = new Lesson()
item.tap()
item.start()
执行结果:
- SyncBailHook
一旦有返回值就会退出
js
constructor(){
// 初始化hooks容器
this.hooks = {
go: new SyncBailHook(['address'])
}
}
执行结果为:
异步
- AsyncParallelHook
异步并行
js
const { AsyncParallelHook, AsyncSeriesHook } = require('tapable')
class Lesson {
constructor(){
this.hooks = {
// 异步并行
leave: new AsyncParallelHook(['name', 'age'])
}
}
tap(){
// 异步绑定方式
this.hooks.leave.tapAsync('class3', (name, age, cb)=>{
setTimeout(()=>{
console.log('class3', name, age)
cb()
}, 2000)
})
// 以promise的形式进行绑定
this.hooks.leave.tapPromise('class4', (name, age)=>{
return new Promise(resolve=>{
setTimeout(() => {
console.log('class4', name, age)
resolve()
}, 1000);
})
})
}
start(){
this.hooks.leave.callAsync('boll', 24, function(){
console.log('end~~')
})
}
}
const item = new Lesson()
item.tap()
item.start()
执行结果:
- AsyncSeriesHook
异步串行
js
constructor() {
// 初始化hooks容器
this.hooks = {
// AsyncSeriesHook: 异步串行
leave: new AsyncSeriesHook(['name', 'age'])
}
}
执行结果:
按照异步的书写顺序进行执行,不按照设置时间
二. compiler钩子
Compiler
模块是 webpack 的主要引擎,它通过 CLI 传递的所有选项, 或者 Node API,创建出一个 compilation
实例。 它扩展(extend
)自 Tapable
类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler
上注册。
compiler
对象代表了完整的 webpack
环境配置。这个对象在启动 webpack
时被一次性建立,并配置好所有可操作的设置,包括 options
,loader
和 plugin
。当在 webpack 环境中应用一个插件时,插件将收到此 compiler
对象的引用。可以使用 compiler
来访问 webpack
的主环境。
compile的内部实现
js
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
/** @type {SyncBailHook<Compilation>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<Stats>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {AsyncSeriesHook<>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<Compiler>} */
......
......
some code
};
......
......
some code
}
可以看到, Compier
继承了Tapable
, 并且在实例上绑定了一个hook
对象, 使得Compier
的实例compier
可以像这样使用:
js
class Plugins1 {
apply(complier){
complier.hooks.emit.tap('Plugins1', compilation=>{
console.log('emit.tap')
})
complier.hooks.emit.tapAsync('Plugins1', (compilation, cb)=>{
setTimeout(() => {
console.log('emit.tapAsync')
cb()
}, 1000);
})
complier.hooks.emit.tapPromise('Plugins1', (compilation)=>{
return new Promise(resolve=>{
setTimeout(() => {
console.log('emit.tapPromise')
resolve()
}, 1000);
}, 1000)
})
complier.hooks.afterEmit.tap('Plugins1', compilation=>{
console.log('emit.tap')
})
complier.hooks.done.tap('Plugins1', complier=>{
console.log('emit.tap')
})
}
}
module.exports = Plugins1
执行结果:
compier常用事件钩子
三. compilation
Compilation
模块会被 Compiler
用来创建新的 compilation
对象(或新的 build
对象)。
compilation
对象代表了一次资源版本构建。当运行 webpack
开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation
,从而生成一组新的编译资源。一个 compilation
对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation
对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
添加文件plugins
创建plugins2文件:
js
class Plugins2 {
apply(compiler){
compiler.hooks.thisCompilation.tap('Plugins2', compilation=>{
compilation.hooks.additionalAssets.tapAsync('Plugins2', (cb)=>{
cb()
})
})
}
}
module.exports = Plugins2
- 方式一
直接设置静态文件
js
class Plugins2 {
apply(compiler){
// 初始化compilation钩子
compiler.hooks.thisCompilation.tap('Plugins2', compilation=>{
// 添加资源
compilation.hooks.additionalAssets.tapAsync('Plugins2', (cb)=>{
const content = 'hello burvs'
// 往要输出资源中,添加一个a.txt
compilation.assets['a.txt'] = {
// 文件大小
size(){
return content.length
},
// 文件内容
source(){
return content
}
}
cb()
})
})
}
}
module.exports = Plugins2
- 方式二
使用compilation.assets
和fs.readFile
读取文件
读取动态设置的文件
js
const fs = require('fs')
const path = require('path')
const util = require('util')
const webpack = require('webpack')
const { RawSource } = webpack.sources
// 将fs.readFile方法变成基于promise风格的异步方法
const readFile = util.promisify(fs.readFile)
class Plugins2 {
apply(compiler){
compiler.hooks.thisCompilation.tap('Plugins2', compilation=>{
compilation.hooks.additionalAssets.tapAsync('Plugins2', async (cb)=>{
const data = await readFile(path.resolve(__dirname, 'b.txt'))
compilation.assets['b.txt'] = new RawSource(data)
// 方式三:使用compilation.emitAsset方法
compilation.emitAsset('b.txt', new RawSource(data));
cb()
})
})
}
}
module.exports = Plugins2
compilation常用事件钩子
事件钩子 | 触发时机 | 参数 | 类型 |
---|---|---|---|
normal-module-loader | 普通模块 loader,真正(一个接一个地)加载模块图(graph)中所有模块的函数。 | loaderContext module | SyncHook |
seal | 编译(compilation)停止接收新模块时触发。 | - | SyncHook |
optimize | 优化阶段开始时触发。 | - | SyncHook |
optimize-modules | 模块的优化 | modules | SyncBailHook |
optimize-chunks | 优化 chunk | chunks | SyncBailHook |
additional-assets | 为编译(compilation)创建附加资源(asset)。 | - | AsyncSeriesHook |
optimize-chunk-assets | 优化所有 chunk 资源(asset)。 | chunks | AsyncSeriesHook |
optimize-assets | 优化存储在 compilation.assets 中的所有资源(asset) | assets | AsyncSeriesHook |
四. 实现plugin
CopyWebpackPlugin实现拷贝目录文件。
- webpack.config.js文件:
js
const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin')
module.exports = {
plugins: [
new CopyWebpackPlugin({
// 从哪个文件下拷贝
from: 'public',
// 拷贝到哪里去
to: 'css',
// 忽略那些文件的拷贝
ignore: ['**/index.html']
})
]
}
schema.json文件:(用于制定options参数规则)
js
{
"type": "object",
"properties": {
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"ignore": {
"type": "array"
}
},
"additionalProperties": false
}
CopyWebpackPlugin.js文件:
js
const path = require('path');
const fs = require('fs');
const {promisify} = require('util')
const { validate } = require('schema-utils');
const globby = require('globby');
const webpack = require('webpack');
const schema = require('./schema.json');
const { Compilation } = require('webpack');
const readFile = promisify(fs.readFile);
const {RawSource} = webpack.sources
class CopyWebpackPlugin {
constructor(options = {}) {
// 验证options是否符合规范
validate(schema, options, {
name: 'CopyWebpackPlugin'
})
this.options = options;
}
apply(compiler) {
// 初始化compilation
compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
// 添加资源的hooks
compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
// 将from中的资源复制到to中,输出出去
const { from, ignore } = this.options;
const to = this.options.to ? this.options.to : '.';
// context就是webpack配置
// 运行指令的目录
const context = compiler.options.context; // process.cwd()
// 将输入路径变成绝对路径
const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);
// 1. 过滤掉ignore的文件
// globby(要处理的文件夹,options)
const paths = await globby(absoluteFrom, { ignore });
console.log(paths); // 所有要加载的文件路径数组
// 2. 读取paths中所有资源
const files = await Promise.all(
paths.map(async (absolutePath) => {
// 读取文件
const data = await readFile(absolutePath);
// basename得到最后的文件名称
const relativePath = path.basename(absolutePath);
// 和to属性结合
// 没有to --> reset.css
// 有to --> css/reset.css
const filename = path.join(to, relativePath);
return {
// 文件数据
data,
// 文件名称
filename
}
})
)
// 3. 生成webpack格式的资源
const assets = files.map((file) => {
const source = new RawSource(file.data);
return {
source,
filename: file.filename
}
})
// 4. 添加compilation中,输出出去
assets.forEach((asset) => {
compilation.emitAsset(asset.filename, asset.source);
})
cb();
})
})
}
}
module.exports = CopyWebpackPlugin;
输出前public目录:
输出目录:
- package.json文件
json
{
"name": "04.plugin",
"version": "1.0.0",
"description": "",
"main": "tapable.test.js",
"scripts": {
"start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"globby": "^11.0.1",
"schema-utils": "^3.0.0",
"tapable": "^2.0.0",
"webpack": "^5.1.3",
"webpack-cli": "^4.0.0"
}
}
写在最后 ⛳
未来可能会更新实现mini-react
和antd
源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳