一、全流程
这里结合一张流程图了解一下 enhanced-reolve 的工作全貌:

一、注册流水线
我们大致回故一下从今天的流水线内容:
- resolve 开始解析:注册 UnsafeCachePlugin、ParsePlugin 插件;
- paresed-resolve:request 解析阶段;
- described-resolve 描述文件已解析;
- raw-resolve: 原始解析阶段,处理 alias/aliasFields/extension/extensionAlias;
- normal-resolve: 普通解析阶段,处理 preferRealative、preferAbsolute
- internal: 内部解析阶段,处理 importsFields 选项声明的能力;
- raw-module 阶段,处理 resolve.modules、exportsFields 选项声明的能力;
- module:处理带有 @ 符号的路径;
- resolve-as-module:处理 resolveToContext 配置项目;
- undescribed-resolve-in-package:重新读取 package.json;
- resolve-in-package:处理 mexportsFields 配置;
- resolve-in-existing-directory:
- relative:注册 DescriptionFilePlugin 插件;
- described-relative:target 为 directory hook;
- directory:注册 DirectoryExistsPlugin 插件;
- undescribed-existing-directory:根据有无 resolveToContext 配置注册不同插件;
- described-existing-directory:注册 MainFieldPlugin 和 UseFilePlugin;
- raw-file:处理扩展名相关,尝试匹配;
- file:处理 alias
- final-file:处理各种 dependencies;
- resolved:处理 restrictions 配置,符合就返回否则报错;
二、注册插件
本文按照 ehanced-resolve 执行的流程注讲解其插件系统,共包含了 23 个插件,以下为各个插件及其作用:
- ParsePlugin:该插件用于解析原始的 request;
- DescriptionFilePlugin:读取 package.json 文件;
- NextPlugin:从一个 source 钩子推进到 target 钩子;
- AliasPlugin:处理 fallback、alias 等;
- AliasFieldPlugin:处理 package.json.browser 字段;
- ExtensionAliasPlguin:拼接各个扩展名并尝试解析;
- JoinRequestPlugin:拼接请求,把 request.path 和 request 拼接;
- ConditionalPlugin:判断本次 request 的类型是否满足条件;
- RootsPlugin: 把的第一个 /(表示根目录) 改写成 resolve.roots 设定的目录;
- ImportsFieldPlugin:这个插件用于处理包的内部 request;
- SelfReferencePlugin:作用是结合 exportsFields 配置项和 package.json.exports 改写默认的 main 和子路径的(映射成别的路径);
- ModulesInHierarchicalDirectoriesPlugin:这个插件用作从当前目录一直向上查 node_modules,一直找到 / 目录下的 node_modules 终止,中间如果找到就中断;
- PnpPlugin:这个我也不会,也懒得看 👻👻👻👻👻👻👻👻
- JoinRequestPartPlugin:这个插件简化 @namespace/a/b/c 这种 request 为相对路径;
- DirectoryExistsPlugin:这个插件的作用主要是判断截止到当前流程,path 是不是一个真实存在的目录;
- ExportsFieldPlugin:结合 package.json.exports 字段是用于重新定义模块的导出规则;
- UseFilePlugin:处理导入目录时自动解析到目录下的 index.js;
- MainFieldPlugin:这个插件的作用是把包名和 package.json.main 字段解析拼接;
- TryNextPlugin:就是单纯的引导到下个钩子;
- AppendPlugin:给 path 和 relativePath 追加各个可能得扩展名;
- FileExistsPlugin:获取文件路径,检查其对应的 fs.stat 结果;
- SymlinkPlugin:将 path 当做一个 symlink 尝试读取,读到内容就是 symlink;
- ResultPlugin:触发 resolver.hooks.result 钩子,返回解析结果;
三、思考
本文到治理算是完成了 ehanced-resolve 的全部讲解工作,最后我们回过头来思考一个问题:
为啥要有 enhanced-resolve?
这个问题我曾困惑了很久,到这里能回答这问题了:
答:因为完备且具备扩展性;
3.1 模块规范的差异
说到解析规则,大家对 require.resolve 一定不陌生。初次接触 ehanced-resolve 的时候,我一直很好奇为啥有 require.resolve() 就能解决的问题,为啥还要整这么多的活儿?
起初我以为是效率,后来证明是错误的,使用 enhanced-resolve 非但不快反而慢。后来经大神提示是因为完备可扩展。
前端因为其特殊的历史原因,ECMA 早期没有实现模块的规范,导致历史上的模块规范都是社区的产物,包括 AMD/CMD/UMD.... 各家规范相对独立,像前面提及的 require.resolve 只是 CommonJS 在 Node.JS 内部实现的解析,其他环境则没有这个能力,或者需要 runtime 去实现一套。
而 webpack 作为构建工具,没打算限制用户的模块规范。因此它需要去抹平各个规范下的模块定义和解析规则,使得无论用户使用哪种规范,webpack 都能正确的理解并在打包时获取到符合用户预期的模块。
ehanced-resolve 完全实现了 CommonJS 和 ESM 的解析规则,无论你的项目中使用某一种或者某几种,webpack 借助 ehanced-resolve 都可以覆盖掉方方面面的路径解析场景。
3.2 可扩展性
思考一下,你在日常开发中遇到过哪些需要处理路径解析的场景?
- 使用别名,简化导入时的路径,提升开发效率,降低错误引用的几率;
- 跨平台项目构建时,自动取用不同对应平台配置;
- 极端场景下,用自己的包替代三方包或者其中的某些模块;
- 出于某种原因,你需要修改模块的解析结果路径;
- ....
这只是列举了几个很常见场景的例子,那在日常开发中,enhanced-resolve 可以扩展模块解析 这个阶段的能力。比如 webpack 通过配置支持 alias、extension 在内的各种简洁的能力,大大降低开发成本。
此外,enhanced-resolve 创建的 Resolver 同样继承自 tappable 意味着它内部提供的这些钩子同样可以订阅,这就为我们开发者打开了介入解析工作流程的机会。
比如,你有一个跨平台的项目,当 mode=wx 时,自动取用 .wx.js 后缀的模块,mode=ali 时自动取用 .ali.js 模块,当然如果都没有再取用普通的 .js 模块;
这个例子取用的是 Mpx 框架中的已经实现的能力:Mpx 中的 Mode 支持
module.exports = class AddModePlugin {
constructor (source, mode, fileConditionRules, target) {
this.source = source
this.target = target
this.mode = mode
this.fileConditionRules = fileConditionRules
}
apply (resolver) {
const target = resolver.ensureHook(this.target)
const mode = this.mode
resolver.getHook(this.source).tapAsync('AddModePlugin', (request, resolveContext, callback) => {
if (request.mode || request.env) {
return callback()
}
const obj = {
mode
}
const resourcePath = request.path
let extname = ''
if (resourcePath.endsWith(JSON_JS_EXT)) {
extname = JSON_JS_EXT
} else {
extname = path.extname(resourcePath)
}
// 当前资源没有后缀名或者路径不符合fileConditionRules规则时,直接返回
if (!extname || !matchCondition(resourcePath, this.fileConditionRules)) return callback()
const queryObj = parseQuery(request.query || '?')
queryObj.mode = mode
queryObj.infix = `${queryObj.infix || ''}.${mode}`
obj.query = stringifyQuery(queryObj)
obj.path = addInfix(resourcePath, mode, extname)
obj.relativePath = request.relativePath && addInfix(request.relativePath, mode, extname)
resolver.doResolve(target, Object.assign({}, request, obj), 'add mode: ' + mode, resolveContext, callback)
})
}
}
除了前面提及的完备性,webpack 强大可扩展性在路径解析这个领域得到了延续!