前言
通过插件我们可以扩展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 { //监听文件变化列表信息 ... } }