webpack 插件工作流程和原理
通过插件我们可以扩展 webpack,在合适的时机通过 Webpack 提供的 API 改变输出结果,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。
- 想要了解 webpack 的插件的机制,需要弄明白以下几个知识点:
- 一个简单的插件的构成
- webpack 构建流程
- Tapable 是如何把各个插件串联到一起的
- compiler 以及 compilation 对象的使用以及它们对应的事件钩子
一个简单的插件的构成
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
// 使用:
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
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 等等信息
理解事件流机制 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 是如何将 webpack/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
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()
webpack4 核心模块 tapable 源码解析: https://www.cnblogs.com/tugenhua0707/p/11317557.html
理解 Compiler(负责编译)
Compiler 对象包含了当前运行 Webpack 的配置,包括 entry、output、loaders 等配置,这个对象在启动 Webpack 时被实例化,而且是全局唯一的。Plugin 可以通过该对象获取到 Webpack 的配置信息进行处理。
apply 方法中插入钩子的一般形式如下:
// compiler提供了compiler.hooks,可以根据这些不同的时刻去让插件做不同的事情。
compiler.hooks.阶段.tap函数('插件名称', (阶段回调参数) => {})
compiler.run(callback)
理解 Compilation
Compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。 简单来说,Compilation 的职责就是构建模块和 Chunk,并利用插件优化构建过程。
介绍几个常用的 Compilation Hooks
buildModule(SyncHook): 在模块开始编译之前触发,可以用于修改模
succeedModule(SyncHook): 在模块开始编译之前触发,可以用于修改模块
finishModules(AsyncSeriesHook): 当所有模块都编译成功后被调用
seal(SyncHook): 当一次 compilation 停止接收新模块时触发
optimizeDependencies(SyncBailHook): 在依赖优化的开始执行
optimize(SyncHook): 在优化阶段的开始执行
optimizeModules(SyncBailHook): 在模块优化阶段开始时执行,插件可以在这个钩子里执行对模块的优化,回调参数:modules
optimizeChunks(SyncBailHook): 在代码块优化阶段开始时执行,插件可以在这个钩子里执行对代码块的优化,回调参数:chunks
optimizeChunkAssets(AsyncSeriesHook): 优化任何代码块资源,这些资源存放在 compilation.assets 上。一个 chunk 有一个 files 属性,它指向由一个 chunk 创建的所有文件。任何额外的 chunk 资源都存放在 compilation.additionalChunkAssets 上。回调参数:chunks
optimizeAssets(AsyncSeriesHook): 优化所有存放在 compilation.assets 的所有资源。回调参数:assets
Compiler 和 Compilation 的区别
Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译,只要文件有改动,compilation 就会被重新创建。
- webpack 打包过程或者插件代码里该如何调试?
node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress
其中参数--inspect-brk 就是以调试模式启动 node