plugin
webpack打包是一种事件流的机制,在构建的过程当中埋下了很多hook,利用tapable,能让这些hook在webpack的事件流上有条不紊的运行。
也就是说,Webpack 在运行过程中会广播事件,plugin只需要监听它所关心的事件,就能加入到这条 webapck 机制中,在特定的时刻调用webpack提供的API执行相应的操作,去改变 webapck 的运作,使得整个系统扩展性良好。
结合我们已经用过的一些插件,比如HtmlWebpackPlugin,我们一般这么使用
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: 'webapp'
})
]
}
通过new去调用一个插件,所以很明显插件就是一个构造函数,或者是一个类,并且我们是可以传递参数进去的。
凡事都有门道,所以一个plugin的结构大概是下面这样的:
function MyPlugin (options) {
// do something here...
}
class MyPlugin {
constructor (options) {
// do something here...
}
}
再进一步,plugin是怎么加入webpack的事件流里的呢?
在webpack源码里找答案
webpack打包前会将我们显式定义的插件,和内置插件合并, 然后调用每个插件的apply方法,并将compiler作为参数传递进去。
所以每个plugin都应该有一个apply方法,那么一个plugin的结构大概是下面这样的(以后用class举例):
class MyPlugin {
constructor (options) {
this.options = options
// do something here...
}
apply(compiler) {
// do something here...
}
}
什么是compiler?
在apply调用时,接受了一个参数compiler,compiler在整个编译生命周期有着至关重要的作用。compiler继承自tapable。
tapable就是hook的核心库,发布,订阅,这些字眼是不是很熟悉,这些实际上类似发布订阅模式,都是注册一个事件,然后到了适当的时候执行。
以我们最常见的node.js的Event机制为例。通过on方法注册一个事件,然后通过emit方法进行触发。
const EventEmitter = require("event");
const myEmitter = new EventEmitter();
myEmitter.on("js",(..args) => {
console.log(...args);
})
myEmitter.emit("js","新年好");
tapable的机制与Event类似,它可以用来定义各种各样的钩子。
大致分为四类
- hook:普通钩子,监听器之间互相独立不干扰
- BailHook:熔断钩子,某个监听返回非undefined时后续不执行
- waterfallHook:瀑布钩子,上一个监听的返回值可传递给下一个
- loopHook:循环钩子,如果当前未返回false则一直执行
具体有以下9种
tapable库同步钩子
- SyncHook
- SyncBailHook
- SyncWaterfallHook
- SyncLoopHook
tapable库异步串行钩子
- AsyncSeriesHook
- AsyncSeriesBailHook
- AsyncSeriesWaterfallHook
tapable库异步并行钩子
- AsyncParalleHook
- AsyncparallBailHook
下面以同步钩子举个例子🌰
const { SyncHook } = require('tapable')
let hook = new SyncHook(['name', 'age'])
hook.tap('fn1', function (name, age) {
console.log('fn1----', name, age)
})
hook.tap('fn2', function (name, age) {
console.log('fn2----', name, age)
})
hook.call('luck', 18)
compiler身上有webpack编译过程中向外暴露的事件流名称,在MyPlugin的apply方法中通过compiler订阅webpack事件流,就可以实现自定义插件
具体有哪些事件呢?还是从源码中找答案
compiler.hooks 是钩子贯穿了整个webpack打包的生命周期,那么我们的插件就是注册到这些钩子(订阅钩子事件),当执行到这些钩子函数时,将会通知插件,并且通过回调返回参数给插件,就可以实现插件逻辑。
以compiler.hooks.done这个hook为例、继续完善plugin的结构:
class MyPlugin {
constructor (options) {
this.options = options
// do something here...
}
apply(compiler) {
compiler.hooks.done.tap(('MyPlugin'), (stats) => {
console.log(stats)
// do something here...
})
}
}
这里写几个重要的节点
webpack的各个阶段以及重要的钩子
| 关键钩子 | 说明 |
|---|---|
| environmenvt | 读取环境 |
| afterEnvironment | 读取环境后触发 |
| beforeRun | 运行前的准备活动,扩展了ompiler文件读取能力 |
| run | 开始执行构建 |
| beforeCompile | beforeCompile开始编译前的准备,创建的ModuleFactory,创建Compilation,并绑定ModuleFactory到Compilation上。同时处理一些不需要编译的模块,比如ExternalModule(远程模块)和DllModule(第三方模块) |
| compile | 进行编译 |
| make | 编译的核心流程 |
| afterCompile | 编译结束 |
| shouldEmit | 确定编译时候成功,是否可以开始输出了。 |
| emit | 输出文件 |
| afterEmit | 输出完毕 |
| done | 所有流程结束 |
同时也需要知道 compilation 这个重要的概念。complier和compilation都是tabable的实例对象,ompilation身上也有很多重要的hook、
Compilation对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。
简单来说,Compilation的职责就是构建模块和Chunk,并利用插件优化构建过程。
值得注意的是,compile是编译过程中创建了一个compilation,我们在一开始并不能获取到compilation,需要在compile创建compilation后再获取compilation身上的hooks。
// compiler.compile 阶段内生成compilation, 在compiler.hooks.make阶段作为参数传入
compiler.hooks.make.tapAsync(
"MyPlugin",
(compilation, callback) => {
// 可以获取到compilation
ccompilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
// 对ccompilation上的hooks进行操作
})
}
);
// 或者后续在compiler的compilation hooks上获取compilation对象
compiler.hooks.compilation.tap("MyPlugin", compilation => {
})
Compiler 和 Compilation 的区别?
- compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
- compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
在开发过程中,我们主要也是用到compiler和compilation身上的hooks来开发plugin,实现一些小而美的功能.
自定义插件
根据以上,一般一个具体的plugin 由下面部分组成:
- 一个具名的JavaScript函数
- 在它的原型上定义apply方法,会接受一个compiler参数
- ( Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。Plugin可以通过该对象获取到Webpack的配置信息进行处理。)
- 指定一个触及到webpack本身的事件钩子
- 操作webpack内部的实例特定数据,实现自己的功能
- 在实现功能后调用webpack提供的callback(必要的情况下,有些hooks不需要callback)
小试牛刀,编写一个插件,在每个产出的js头部添加注释
开发
// 插件输出类
class AddCommentPlugin {
constructor(opts) {
this.opts = opts;
}
apply(compiler) {
// 注册自定义插件钩子到生成资源到 output 目录之前,拿到compilation对象
compiler.hooks.emit.tap('AddCommentPlugin', compilation => {
// 遍历构建产物
Object.keys(compilation.assets).forEach(item => {
// .source()是获取构建产物的文本
// .assets中包含构建产物的文件名
let content = compilation.assets[item].source();
content = content.slice(0,0).concat(this.opts.comment, content);
// console.info(content);
// 更新构建产物对象
compilation.assets[item] = {
source: () => content,
size: () => content.length
}
});
});
}
};
module.exports = AddCommentPlugin;
webpacl.config.js
...
plugins: [
...
new AddCommentPlugin({
comment : '/** 没错,这是一段注释 */'
}),
...
]
结果
/** 没错,这是一段注释 *//******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
官方插件推荐
BannerPlugin
在每个编译产出文件头部添加banner,当然也可以用来做一些变量提示
new webpack.BannerPlugin({
banner: `if ( !window.xxxx ) {throw Error('请引入变量 window.xxxx ');}
`, // 其值为字符串,将作为注释存在
raw: true, // 如果值为 true,将直出,不会被作为注释
entryOnly: false, // 如果值为 true,将只在入口 chunks 文件中添加
}),
CircularDependencyPlugin
检测项目中是否存在循环依赖
const CircularDependencyPlugin = require('circular-dependency-plugin');
new CircularDependencyPlugin({
// exclude: /a\.js|node_modules/,
// include specific files based on a RegExp
include: /node_modules/,
// add errors to webpack instead of warnings
failOnError: true,
// allow import cycles that include an asyncronous import,
// e.g. via import(/* webpackMode: "weak" */ './file.js')
allowAsyncCycles: false,
// set the current working directory for displaying module paths
cwd: process.cwd()
})
clean-webpack-plugin
用于每次打包时,将上次编译产出的dist目录内文件删除
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
new CleanWebpackPlugin()
等等,还有很多插件点击查看官方推荐
DefinePlugin
用于注入全局环境变量
new Webpack.DefinePlugin({
"process.env": JSON.stringify(process.env)
})
常用的plugin我们都可以在官方上找到合适的插件,但是了解原理,我们也可以去根据项目自定义合适的插件帮助我们更好的服务项目,仔细想想,你的项目里有哪些小功能可以使用plugin,自己封装一个,将其用到项目中去。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。