通过enhanced-resolve领略webpack的插件架构
前言
大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.
我的宗旨就是 万物皆可手写
今天我们来看一看webpack内部的核心库enhanced-resolve,对webpack的可插拔架构有一个更深入的认知
新手创作不易,有问题欢迎指出和轻喷,谢谢
enhanced-resolve库
我们可以在webpack源码的依赖中看到这个玩意
// package.json
{
"name": "webpack",
"version": "5.74.0",
"dependencies": {
"enhanced-resolve": "^5.10.0",
...
}
我们也可以在webpack的主类Compiler中看到这个玩意
class Compiler {
constructor(context, options = /** @type {WebpackOptions} */ ({})) {
this.hooks = {}
/** @type {ResolverFactory} */
this.resolverFactory = new ResolverFactory();
}
......
}
看一下Readme,可以发现,这个库主要做webpack模块的路径解析
可以对比node的 require.resolve
和我们的enhanced-resolve
(增强版resolve)
增强的功能
原版require.resolve只能运行在node环境中,而且无法应对webpack多样化的打包需求.
-
可解析更多的后缀 比如
./index
没有后缀,node会解析.js,.json,.node等, 而本包可以配置解析更多的后缀名 -
require.resolve
的返回值只有一个完整路径,enhanced-resolve
的返回值还包含了描述文件等较为丰富的数据。 -
enhanced-resolve
支持配置文件/文件夹别名 -
require.resolve
只会去解析文件的完整路径,但是enhanced-resolve
既可以查询文件也可以查询文件夹。 .....
使用
基本
const { ResolverFactory, CachedInputFileSystem } = require("enhanced-resolve");
const fs = require("fs");
const path = require("path");
const myResolver = ResolverFactory.createResolver({
fileSystem: new CachedInputFileSystem(fs, 4000),
extensions: [".json", ".js", ".ts"],
})
const context = {};
const resolveContext = {};
const lookupStartPath = path.resolve(__dirname);
const request = "./test"; // 访问./test文件 获取路径
myResolver.resolve(
context,
lookupStartPath,
request,
resolveContext,
(err, path, result) => {
if (err) {
console.log("createResolve err: ", err);
} else {// 这里可获得解析好的路径
console.log("createResolve path: ", path);
}
}
);
创建Resolver
resolverFactory是一个典型的工厂模式,通过options创建Resolver并进行修改,最终返回
- 首先我们解析配置options,创建Resolver,(CreateResolver),并挂载hooks
exports.createResolver = function (options) {
// 解析options
const normalizedOptions = createOptions(options);
const {...} = normalizedOptions;
// 准备plugins
const plugins = userPlugins.slice();
// new Resolver
const resolver = customResolver
? customResolver
: new Resolver(fileSystem, normalizedOptions);
// 给resolver挂载各种hooks
resolver.ensureHook("resolve");
....
}
- 其中ensureHook内部会创建一个
AsyncSeriesBailHook
类型的tapableHook并挂载到Resolver上, hook对应了不同的执行流程 AsyncSeriesBailHook是一个异步串行的 Hook:用于注册异步回调函数,并且这些回调函数将按照它们被注册的顺序依次执行。
ensureHook(name) {
...
return (this.hooks[name] = new AsyncSeriesBailHook();
}
画成图也就是这样
3. resolver上的Hook大概有这些(2023年的版本) 而他们会被按照顺序依次执行(AsyncSeriesBailHook)
resolver.ensureHook("resolve");
resolver.ensureHook("internalResolve");
resolver.ensureHook("newInternalResolve");
resolver.ensureHook("parsedResolve");
resolver.ensureHook("describedResolve");
resolver.ensureHook("rawResolve");
resolver.ensureHook("normalResolve");
resolver.ensureHook("internal");
resolver.ensureHook("rawModule");
resolver.ensureHook("module");
resolver.ensureHook("resolveAsModule");
resolver.ensureHook("undescribedResolveInPackage");
resolver.ensureHook("resolveInPackage");
resolver.ensureHook("resolveInExistingDirectory");
resolver.ensureHook("relative");
resolver.ensureHook("describedRelative");
resolver.ensureHook("directory");
resolver.ensureHook("undescribedExistingDirectory");
resolver.ensureHook("existingDirectory");
resolver.ensureHook("undescribedRawFile");
resolver.ensureHook("rawFile");
resolver.ensureHook("file");
resolver.ensureHook("finalFile");
resolver.ensureHook("existingFile");
resolver.ensureHook("resolved");
Hooks们订阅plugins
- 之后我们会将各种内置plugins (根据配置) 推入对应的hook中(hook订阅plugins), plugins会指定下一个流转的hook
// 创建并订阅ParsePlugin(先推入plugins数组)
plugins.push(new ParsePlugin(//
"resolve", // 订阅本插件的hook
resolveOptions,
"parsed-resolve"// 流转的下一个hook
));
// 根据配置判断是否推入
if (options.alias.length > 0) {
plugins.push(new AliasPlugin("raw-resolve", alias, "internal-resolve"));
}
// 推入NextPlugin到after-parsed-resolve Hook中
plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
// 其他插件
......
// 最后会批量订阅plugins数组中的插件到hook中(plugins内部订阅)
for (const plugin of plugins) {
if (typeof plugin === "function") {
plugin.call(resolver, resolver);
} else {
plugin.apply(resolver);
}
}
- 每个plugins内部中都会传入我们的resolver,(想必写过webpack插件的小伙伴都懂,我们自己写的插件中的apply方法会直接接收compiler作为参数,这里则接收resolver)
class ParsePlugin {
constructor(source, requestOptions, target) {}
...
apply(resolver) {
// 获取/创建下一个hook(target为传入的下一个hook名称)
const target = resolver.ensureHook(this.target);
resolver // 获取当前hook,将逻辑tapAsync到该hook中
.getHook(this.source)
.tapAsync("ParsePlugin", (request, resolveContext, callback) => {
//插件的处理逻辑
...
}
}
}
画成图也就是这样
执行Resolver
此时Resolver已经创建完毕 ,经过了两大步骤
- 注册hooks
- hooks订阅各种plugins
还记得使用方法吗?
// 最终会执行resolver.resolve
myResolver.resolve()
进去可以看到
resolve(context, path, request, resolveContext, callback) {
// 最终返回doResolve
return this.doResolve()
}
// doResolve最终会返回hook.callAsync,并执行回调
doResolve(){
return hook.callAsync(request, innerContext, (err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
// 在回调中会流转到下一个hook回调
// 这个callback是tapableHook中触发下一个hook回调的方法,执行doResolve时传入
// (详细请参考tapable中AsyncSeriseHook的用法)
callback();
})
}
画成流程图也就是这样
最后我们可以结合几张图来看这个流程
1.hooks就像是弹夹,依次向下,一个弹夹打空了就换另一个
2.plugin就是子弹,通过回调依次射出(有些是通过nextPlugin进行流转)
3.Resolve方法就是机枪的启动开关
Plugins
最后来看看作为子弹的plugins,他们分别都干了些什么? 可以看到在本库中,(可能)使用到的插件有这些
const AliasFieldPlugin = require("./AliasFieldPlugin");
const AliasPlugin = require("./AliasPlugin");
const AppendPlugin = require("./AppendPlugin");
const ConditionalPlugin = require("./ConditionalPlugin");
const DescriptionFilePlugin = require("./DescriptionFilePlugin");
const DirectoryExistsPlugin = require("./DirectoryExistsPlugin");
const ExportsFieldPlugin = require("./ExportsFieldPlugin");
const ExtensionAliasPlugin = require("./ExtensionAliasPlugin");
const FileExistsPlugin = require("./FileExistsPlugin");
const ImportsFieldPlugin = require("./ImportsFieldPlugin");
const JoinRequestPartPlugin = require("./JoinRequestPartPlugin");
const JoinRequestPlugin = require("./JoinRequestPlugin");
const MainFieldPlugin = require("./MainFieldPlugin");
const ModulesInHierarchicalDirectoriesPlugin = require("./ModulesInHierarchicalDirectoriesPlugin");
const ModulesInRootPlugin = require("./ModulesInRootPlugin");
const NextPlugin = require("./NextPlugin");
const ParsePlugin = require("./ParsePlugin");
const PnpPlugin = require("./PnpPlugin");
const RestrictionsPlugin = require("./RestrictionsPlugin");
const ResultPlugin = require("./ResultPlugin");
const RootsPlugin = require("./RootsPlugin");
const SelfReferencePlugin = require("./SelfReferencePlugin");
const SymlinkPlugin = require("./SymlinkPlugin");
const TryNextPlugin = require("./TryNextPlugin");
const UnsafeCachePlugin = require("./UnsafeCachePlugin");
const UseFilePlugin = require("./UseFilePlugin");
单个插件功能粒度很低,一般也就一百多行代码,可以理解为逻辑块, 这里简单介绍几个
NextPlugin 引导插件到下一个Hook
RootsPlugin 其作用是在解析模块的路径时,将源路径中以斜杠(/)开头的请求与一组根路径进行匹配,并将匹配到的路径替换成根路径加上请求路径而得到的新路径。
ImportsFieldPlugin 其作用是在描述文件中(如package.json)中查找imports字段并根据其内容进行路径解析。
ParsePlugin 请求路径解析插件
DescriptionFilePlugin 查找file的描述文件插件
AliasPlugin 别名解析插件
JoinRequestPlugin 可解析并输出输出path和ralitivePath
ConditionalPlugin 用于判断跳过某个步骤
后记
想必现在大家再看看webpack官网中的那句话 "webpack本身也是由大量的插件所组成的" 是否有了更好的理解
本人也手写了一个简易的enhance-resolve,使用在自己的手写miniWebpack中,并逐渐miniWebpack本体插件化。
想看一下具体的代码实现可以查看我的git仓库。
转载自:https://juejin.cn/post/7212453375085854781