前言
通过插件我们可以扩展webpack
,在合适的时机通过Webpack
提供的 API 改变输出结果,使webpack
可以执行更广泛的任务,拥有更强的构建能力。 本文将尝试探索 webpack
插件的工作流程,进而去揭秘它的工作原理。同时需要你对webpack
底层和构建流程的一些东西有一定的了解。
想要了解 webpack 的插件的机制,需要弄明白以下几个知识点:
- 一个简单的插件的构成
webpack
构建流程Tapable
是如何把各个插件串联到一起的compiler
以及compilation
对象的使用以及它们对应的事件钩子。
插件基本结构
plugins
是可以用自身原型方法apply
来实例化的对象。apply
只在安装插件被Webpack compiler
执行一次。apply
方法传入一个webpck compiler
的引用,来访问编译器回调。
一个简单的插件结构:
class HelloPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options){
}
// Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) {
// 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
// 在功能流程完成后可以调用 webpack 提供的回调函数;
});
// 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知webpack,才会进入下一个处理流程。
compiler.plugin('emit',function(compilation, callback) {
// 支持处理逻辑
// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这不往下执行
callback();
});
}
}
module.exports = HelloPlugin;
安装插件时, 只需要将它的一个实例放到Webpack config plugins
数组里面:
const HelloPlugin = require('./hello-plugin.js');
var webpackConfig = {
plugins: [
new HelloPlugin({options: true})
]
};
先来分析一下webpack Plugin的工作原理
- 读取配置的过程中会先执行
new HelloPlugin(options)
初始化一个 HelloPlugin
获得其实例。 - 初始化
compiler
对象后调用 HelloPlugin.apply(compiler)
给插件实例传入 compiler
对象。 - 插件实例在获取到
compiler
对象后,就可以通过compiler.plugin(事件名称, 回调函数)
监听到 Webpack 广播出来的事件。 并且可以通过 compiler
对象去操作 Webpack
。
webapck 构建流程
在编写插件之前,还需要了解一下Webpack
的构建流程,以便在合适的时机插入合适的插件逻辑。
Webpack的基本构建流程如下:
- 校验配置文件 :读取命令行传入或者
webpack.config.js
文件,初始化本次构建的配置参数 - 生成
Compiler
对象:执行配置文件中的插件实例化语句new MyWebpackPlugin()
,为webpack
事件流挂上自定义hooks
- 进入
entryOption
阶段:webpack
开始读取配置的Entries
,递归遍历所有的入口文件 run/watch
:如果运行在watch
模式则执行watch
方法,否则执行run
方法compilation
:创建Compilation
对象回调compilation
相关钩子,依次进入每一个入口文件(entry
),使用loader对文件进行编译。通过compilation
我可以可以读取到module
的resource
(资源路径)、loaders
(使用的loader)等信息。再将编译好的文件内容使用acorn
解析生成AST静态语法树。然后递归、重复的执行这个过程, 所有模块和和依赖分析完成后,执行 compilation
的 seal
方法对每个 chunk 进行整理、优化、封装__webpack_require__
来模拟模块化操作.emit
:所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets
上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。
// 修改或添加资源
compilation.assets['new-file.js'] = {
source() {
return 'var a=1';
},
size() {
return this.source().length;
}
};
afterEmit
:文件已经写入磁盘完成done
:完成编译
奉上一张滴滴云博客的WebPack
编译流程图,不喜欢看文字讲解的可以看流程图理解记忆
WebPack 编译流程图原图出自:https://blog.didiyun.com/index.php/2019/03/01/webpack/
看完之后,如果还是看不懂或者对缕不清webpack构建流程的话,建议通读一下全文,再回来看这段话,相信一定会对webpack构建流程有很更加深刻的理解。
理解事件流机制 Tapable
webpack
本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。
Webpack
的 Tapable
事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webapck机制中,去改变webapck的运作,使得整个系统扩展性良好。
Tapable
也是一个小型的 library,是Webpack
的一个核心工具。类似于node
中的events
库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。
webpack中最核心的负责编译的Compiler
和负责创建bundles的Compilation
都是Tapable的实例,可以直接在 Compiler
和 Compilation
对象上广播和监听事件,方法如下:
/**
* 广播事件
* event-name 为事件名称,注意不要和现有的事件重名
*/
compiler.apply('event-name',params);
compilation.apply('event-name',params);
/**
* 监听事件
*/
compiler.plugin('event-name',function(params){});
compilation.plugin('event-name', function(params){});
Tapable
类暴露了tap
、tapAsync
和tapPromise
方法,可以根据钩子的同步/异步方式来选择一个函数注入逻辑。
tap
同步钩子
compiler.hooks.compile.tap('MyPlugin', params => {
console.log('以同步方式触及 compile 钩子。')
})
tapAsync
异步钩子,通过callback
回调告诉Webpack
异步执行完毕tapPromise
异步钩子,返回一个Promise
告诉Webpack
异步执行完毕
compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
console.log('以异步方式触及 run 钩子。')
callback()
})
compiler.hooks.run.tapPromise('MyPlugin', compiler => {
return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
console.log('以具有延迟的异步方式触及 run 钩子')
})
})
Tabable用法
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
tapable
简单实现一个 SyncHook
class Hook{
constructor(args){
this.taps = []
this.interceptors = [] // 这个放在后面用
this._args = args
}
tap(name,fn){
this.taps.push({name,fn})
}
}
class SyncHook extends Hook{
call(name,fn){
try {
this.taps.forEach(tap => tap.fn(name))
fn(null,name)
} catch (error) {
fn(error)
}
}
}
tapable
是如何将webapck/webpack
插件关联的?
Compiler.js
const { AsyncSeriesHook ,SyncHook } = require("tapable");
//创建类
class Compiler {
constructor() {
this.hooks = {
run: new AsyncSeriesHook(["compiler"]), //异步钩子
compile: new SyncHook(["params"]),//同步钩子
};
},
run(){
//执行异步钩子
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled);
});
},
compile(){
//执行同步钩子 并传参
this.hooks.compile.call(params);
}
}
module.exports = Compiler
MyPlugin.js
const Compiler = require('./Compiler')
class MyPlugin{
apply(compiler){//接受 compiler参数
compiler.hooks.run.tap("MyPlugin", () => console.log('开始编译...'));
compiler.hooks.compile.tapAsync('MyPlugin', (name, age) => {
setTimeout(() => {
console.log('编译中...')
}, 1000)
});
}
}
//这里类似于webpack.config.js的plugins配置
//向 plugins 属性传入 new 实例
const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin]
}
let compiler = new Compiler(options)
compiler.run()
想要深入了解tapable
的文章可以看看这篇文章:
webpack4
核心模块tapable
源码解析: https://www.cnblogs.com/tugenhua0707/p/11317557.html
理解Compiler(负责编译)
开发插件首先要知道compiler
和 compilation
对象是做什么的
Compiler
对象包含了当前运行Webpack
的配置,包括entry、output、loaders
等配置,这个对象在启动Webpack
时被实例化,而且是全局唯一的。Plugin
可以通过该对象获取到Webpack的配置信息进行处理。
如果看完这段话,你还是没理解compiler
是做啥的,不要怕,接着看。 运行npm run build
,把compiler
的全部信息输出到控制台上console.log(Compiler)
。
compiler
// 为了能更直观的让大家看清楚compiler的结构,里面的大量代码使用省略号(...)代替。
Compiler {
_pluginCompat: SyncBailHook {
...
},
hooks: {
shouldEmit: SyncBailHook {
...
},
done: AsyncSeriesHook {
...
},
additionalPass: AsyncSeriesHook {
...
},
beforeRun: AsyncSeriesHook {
...
},
run: AsyncSeriesHook {
...
},
emit: AsyncSeriesHook {
...
},
assetEmitted: AsyncSeriesHook {
...
},
afterEmit: AsyncSeriesHook {
...
},
thisCompilation: SyncHook {
...
},
compilation: SyncHook {
...
},
normalModuleFactory: SyncHook {
...
},
contextModuleFactory: SyncHook {
...
},
beforeCompile: AsyncSeriesHook {
...
},
compile: SyncHook {
...
},
make: AsyncParallelHook {
...
},
afterCompile: AsyncSeriesHook {
...
},
watchRun: AsyncSeriesHook {
...
},
failed: SyncHook {
...
},
invalid: SyncHook {
...
},
watchClose: SyncHook {
...
},
infrastructureLog: SyncBailHook {
...
},
environment: SyncHook {
...
},
afterEnvironment: SyncHook {
...
},
afterPlugins: SyncHook {
...
},
afterResolvers: SyncHook {
...
},
entryOption: SyncBailHook {
...
},
infrastructurelog: SyncBailHook {
...
}
},
...
outputPath: '',//输出目录
outputFileSystem: NodeOutputFileSystem {
...
},
inputFileSystem: CachedInputFileSystem {
...
},
...
options: {
//Compiler对象包含了webpack的所有配置信息,entry、module、output、resolve等信息
entry: [
'babel-polyfill',
'/Users/frank/Desktop/fe/fe-blog/webpack-plugin/src/index.js'
],
devServer: { port: 3000 },
output: {
...
},
module: {
...
},
plugins: [ MyWebpackPlugin {} ],
mode: 'production',
context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin',
devtool: false,
...
performance: {
maxAssetSize: 250000,
maxEntrypointSize: 250000,
hints: 'warning'
},
optimization: {
...
},
resolve: {
...
},
resolveLoader: {
...
},
infrastructureLogging: { level: 'info', debug: false }
},
context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin',//上下文,文件目录
requestShortener: RequestShortener {
...
},
...
watchFileSystem: NodeWatchFileSystem {
//监听文件变化列表信息
...
}
}