Webpack5源码解读系列5 - 高可拓展Plugin机制
Plugin介绍 & 使用
Plugin介绍
插件(Plugin)的概念
插件,又称做 Plug-in 或者 addin、addon 等,是一种遵循一定规范的应用程序接口编写出来的程序,从而可以为系统扩展原本不存在的特性。同时,如果一个系统支持了插件体系,也就拥有了可以实现用户自定义化的功能。
Plugin
(插件)是Webpack
实现的基础,Webpack
运行时在各个阶段都会抛出事件,Plugin
通过捕获这些事件完成功能。
在Webpack
内部,除构建主流程之外其他能力均通过Plugin
机制完成,初始化Webpack
时会根据传入配置项注入相对应Plugin
,从而完成配置项工作。除此之外,Webpack
允许用户使用第三方Plugin
拓展功能。整体上来说Plugin
机制让Webpack
具有高拓展特性。
Webpack
提供了Plugin
规范的接口协议,插件既可以以类形式编写,也可以以对象形式编写:
interface PluginInterface {
apply: (compiler: Compiler) => void;
}
Plugin
可在apply
方法内部往传入的compiler
实例注册事件,从而拓展编译器能力。
Plugin使用
Webpack
配置项中提供plugins
字段,用户可通过该字段注册自定义插件,如使用html-webpack-plugin
的配置项如下:
const path = require('path');
const webpackHtmlPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new webpackHtmlPlugin()
],
}
Plugin实现原理
Plugin
执行时行为的本质是一个事件订阅,Webpack
在运行各个阶段会做事件发布,从而完成插件能力,该过程中事件能力由Tapable库提供,了解Plugin
之前我们先了解Tapable
。
Tapable机制
想更全面了解Tapable机制的同学可以查阅理解Webpack不得不提的Tapable
Tapable是专门为插件机制提供事件钩子注册/发布的库,为同步或异步场景提供了10种事件发布模型:
按照执行过程可以将这10种模型分为4种类型钩子,分别是普通钩子、保险(bail)钩子、循环钩子、瀑布流钩子。同步和异步串行都有这四种类型钩子,而异步并行则因为执行特点则仅有普通、保险钩子。
- 普通钩子
普通钩子类型是按照注册顺序执行钩子函数,并不关心返回值如何:
- 保险钩子
保险钩子是在同步钩子的基础上新增判断钩子返回值能力,一旦返回值不为undefined则会跳过所有钩子执行。
- 循环钩子
循环钩子机制有点类似于保险钩子,但是有一点不同:当钩子返回内容不为undefined时,会重新开始钩子执行而非中断执行。
- 瀑布流钩子
瀑布钩子是在同步钩子的基础上将前面钩子的返回值作为下一个钩子执行结果:
Tapable
在Webpack
应用非常广泛,Compiler
、Compilation
以及其他能力内随处可见使用Tapable
管理事件,现在有了Tapable
概念之后,我们看一下Plugin
机制的实现。
Plugin实现
前置名词解释:
Compiler
和Compilation
分别代表什么?它们有什么关系?
Compiler
:在Webpack
运行期间常驻对象,内部可创建一个或者多个Compilation
对象完成构建任务(watch
模式下可能产生多次构建)。Compilation
:Compiler
产生构建任务时会新建一个Compilation
对象并完成构建任务。
基于前面Tapable
的介绍,我们已经对插件运行机制有了一定的了解,Webpack
分别在Compiler
和Compilation
对象上提供事件钩子注册对象实现能力拓展,这里浅看一下代码即可:
Compiler
对象
Compilation
对象
前面提到Plugin
需要遵循插件规范才能够被Webpack
处理,在apply
方法中插件会传入Compiler
实例,可以在Compiler
实例上注册钩子:
class CustomPlugin {
apply(compiler: Compiler) {
// do something
compiler.hooks.xxxx.tap(xxx)
}
}
在Compiler
上有个叫做compilation
的钩子,可用该钩子往Compilation
上注册事件,事件回调中第一个参数是Compilation
实例,如下面例子:
compiler.hooks.compilation.tap(
class CustomPlugin {
apply(compiler: Compiler) {
compiler.hooks.compilation.tap('CustomHook', (compilation) => {});
}
}
Plugin
机制整体处理流程图如下:
实例讲解
EntryOptionPlugin
EntryOptionPlugin
提供处理入口配置项能力,Webpack
核心流程并没有处理入口配置项逻辑能力,而是将其“外包”出去,使用Plugin
承接处理入口配置项任务,假设有如下配置项:
module.exports = {
entry: {
index: "./src/js/index.js",
home: './src/js/Home.js',
},
}
经过入口配置项格式化之后会处理为如下:
module.exports = {
entry: {
index: {
import: "./src/js/index.js",
},
home: {
import: './src/js/Home.js',
},
},
}
之后便交给EntryOptionPlugin
处理配置项内容:
class EntryOptionPlugin {
/**
* @param {Compiler} compiler the compiler instance one is tapping into
* @returns {void}
*/
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
static applyEntryOption(compiler, context, entry) {
if (typeof entry === "function") {
const DynamicEntryPlugin = require("./DynamicEntryPlugin");
new DynamicEntryPlugin(context, entry).apply(compiler);
} else {
// 以对象形式配置,所以走此分支
const EntryPlugin = require("./EntryPlugin");
for (const name of Object.keys(entry)) {
const desc = entry[name];
const options = EntryOptionPlugin.entryDescriptionToOptions(
compiler,
name,
desc
);
for (const entry of desc.import) {
new EntryPlugin(context, entry, options).apply(compiler);
}
}
}
}
static entryDescriptionToOptions(compiler, name, desc) {
// 无需关注
}
}
EntryOptionPlugin
内部判断配置项为非函数类型,判断为同步入口,交由EntryPlugin
处理:
class EntryPlugin {
constructor(context, entry, options) {
this.context = context;
this.entry = entry;
this.options = options || "";
}
apply(compiler) {
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
}
static createDependency(entry, options) {
const dep = new EntryDependency(entry);
dep.loc = { name: typeof options === "object" ? options.name : options };
return dep;
}
}
重点关注两段代码:
- 往
dependencyFactories
注入EntryDependency
对应的Module
工厂,简单来说是当Compilation
解析到入口文件时,会实例化一个描述入口文件的Module
模型。
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
- 创建
EntryDependency
实例,并将其加入到Compilation
入口,作为应用解析起点,这样Compilation
在make
阶段就可以根据起点递归解析整个应用,并将所有文件都处理为Module
。
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
Plugin
机制在Webpack
中应用特别广泛,Webpack
除了打包过程中的模块读取、Chunk分析、代码生成、资源输出等关于流程上的代码由Compiler
和Compilation
实现以外,其他具体的能力均通过Plugin
机制完成,所以Webpack
具有很强的可拓展性。
小结
Plugin
减少了Webpack
功能模块对核心模块的侵入性,给予了Webpack
很强的自定义能力,在Webpack
内部除了主流程之外其他能力均由Plugin
完成,甚至连配置项最终都会转为Plugin
,同时Webpack
还提供自定义注册插件,用户可通过config.plugin
字段配置第三方插件。
转载自:https://juejin.cn/post/7231809738203201594