一、前文回顾
上文中我们完成了 ehanced-resolve 内部的有关各个流水线的注册,主要完成的是后半段的部部分:从 module 到 resolved 的过程:
- 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 配置,符合就返回否则报错;
另外,其设计的 hook 系统是 pipeline 形式的,有别于 webpack 的,这一点大家还是需要注意的!
二、createResolver 注册的插件
从这篇开始我们开始讲解注册在各个阶段的的 enhanced-resolve 插件,学习这个插件将学习它解析路径的核心流程。
2.1 ParsePlugin
该插件用于解析原始的 request 字符串中的信息,具体实现如下:
向 resolver.hooks.resolve/internal-resolve(newResolve/newInternalResolve)钩子注册事件。在事件 handler 中,解析 request.request ,将其中的查询字符串、fragment(#fragment)等解析成对象 obj:{ query, fragment }
。
得到 obj 后,调用 doResolve 将结果传递到 target 钩子:parsedResolve(parsed-resolve)
2.2 DescriptionFilePlugin
描述文件插件,通过向 resolver.hooks.parsedResolve 钩子注册事件:读取 package.json 文件中的内容并且计算模块描述文件的绝对路径、根目录、以及相对路径;
js
const obj = {
...request,
descriptionFilePath: result.path, // 描述文件的绝对路径
descriptionFileData: result.content, // 描述文件的内容
descriptionFileRoot: result.directory, // 该描述文件的根目录
relativePath: relativePath // 相对于项目目录的相对路径
};
最终结果如图:
其中:
- descriptionFilePath: 描述文件的绝对路径
- descriptionFileData: 描述文件的内容
- descriptionFileRoot: 该描述文件的根目录
- relativePath: 相对于项目目录的相对路径
再得到 obj 以后,通过 resolver.doResolve() 将结果传递给 described-resolve(describedResolve)
钩子;
2.3 NextPlugin
NextPlugin 的作用就是单纯将 resolver 的 resolve 流程从一个 source 钩子推进到 target 钩子;
js
module.exports = class NextPlugin {
constructor(source, target) {
this.source = source;
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("NextPlugin", (request, resolveContext, callback) => {
resolver.doResolve(target, request, null, resolveContext, callback);
});
}
};
2.4 AliasPlugin
AliasPlugin 的注册有以下情况:
- webpackConfig.resolve.fallback 配置,即【二、Factory.createResolver《7.....》】情形,处理模块解析失败重定向到兜底模块,比如处理 webpack 对 Node.js 原生模块的 pollyfill
- webpackConfig.resolve.alias 配置,说明对一些路径进行别名设置,比如大家常见的场景
@src/components/some.vue
这种操作,@src 就是你项目下的 src 目录的别名,别名的目的是简化组件、模块导入时的路径认知成本。
AliasPlugin 的工作原理:
- 首先在插件注册时拿到标准化的别名和真实路径的映射集合 this.options 数组;这个数组中项为:
{ name: 'Src', alias: '/User/rmb/proj-dir/src' }
,这个对象下称 item - 当插件的 handler 执行的时候则遍历 this.options ,判断如果 request 以 item.name 开头,尝试用当前 item.alias 替换掉路径中的别名,然后调用 resolver.doResolve 推动解析执行流程到 target 钩子。简言之就是将别名替换成其对应的真实路径,然后重新执行解析流程
js
module.exports = class AliasPlugin {
constructor(source, options, target) {
this.source = source;
this.options = Array.isArray(options) ? options : [options];
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
const getAbsolutePathWithSlashEnding = maybeAbsolutePath => {
const type = getType(maybeAbsolutePath);
if (type === PathType.AbsolutePosix || type === PathType.AbsoluteWin) {
return resolver.join(maybeAbsolutePath, "_").slice(0, -1);
}
return null;
};
const isSubPath = (path, maybeSubPath) => {
const absolutePath = getAbsolutePathWithSlashEnding(maybeSubPath);
if (!absolutePath) return false;
return path.startsWith(absolutePath);
};
resolver
.getHook(this.source)
.tapAsync("AliasPlugin", (request, resolveContext, callback) => {
const innerRequest = request.request || request.path;
if (!innerRequest) return callback();
forEachBail(
this.options, // options 是 fallback 数组
(item, callback) => {
let shouldStop = false;
if (
innerRequest === item.name ||
(!item.onlyModule &&
(request.request
? innerRequest.startsWith(`${item.name}/`) // 如果有 request.request 就要判断是否以别名开头,'Src/s/a' Src 就是别名
: isSubPath(innerRequest, item.name)))
) {
const remainingRequest = innerRequest.substr(item.name.length); // 除了 alias 以外的别名 Src/s/a 的 remainingRequest 为 /s/a
const resolveWithAlias = (alias, callback) => { // 这个 alias 入参是真实路径,callback 是 stoppingCallback 函数
if (alias === false) {
/** @type {ResolveRequest} */
const ignoreObj = {
...request,
path: false
};
if (typeof resolveContext.yield === "function") {
resolveContext.yield(ignoreObj);
return callback(null, null);
}
return callback(null, ignoreObj);
}
if (
innerRequest !== alias &&
!innerRequest.startsWith(alias + "/") // innerRequest 是带有 别名 的 request 字符串,这个字符串不是以真实路径开头,如果是以alias(真实路径)开头的就没必要走下面的流程了
) {
shouldStop = true;
const newRequestStr = alias + remainingRequest;
const obj = {
...request,
request: newRequestStr,
fullySpecified: false
};
return resolver.doResolve(
target,
obj,
"aliased with mapping '" +
item.name +
"': '" +
alias +
"' to '" +
newRequestStr +
"'",
resolveContext,
(err, result) => { // 这下面的 callback 都是 stoppingCallback
if (err) return callback(err);
if (result) return callback(null, result);
return callback();
}
);
}
return callback();
};
const stoppingCallback = (err, result) => {
// 这里面的 callback 是 forEachBail 的控制下一个的 callback
if (err) return callback(err);
if (result) return callback(null, result);
// Don't allow other aliasing or raw request
if (shouldStop) return callback(null, null);
return callback();
};
if (Array.isArray(item.alias)) {
return forEachBail(
item.alias,
resolveWithAlias,
stoppingCallback
);
} else {
return resolveWithAlias(item.alias, stoppingCallback);
}
}
return callback(); // 这个 callback 是 forEachBail 的下一个 callback
},
callback
);
});
}
};
2.5 AliasFieldPlugin
实现原理也很简单就是用当面 request 简化后的请求,去匹配当前 npm 包的 package.json.browser 字段里面的对应的值:
- 直接替换掉 main 的,browser 是字符串的情况
- browser 是对象,取值 data =
browser[简化请求]
- 判断 data 不是 undefined/false 就正常引导至 target 钩子,因为如果是 false 表示屏蔽(忽略)这个模块,undefined 表示无效处理。
js
const DescriptionFileUtils = require("./DescriptionFileUtils");
const getInnerRequest = require("./getInnerRequest");
module.exports = class AliasFieldPlugin {
constructor(source, field, target) {
this.source = source;
this.field = field;
this.target = target;
}
/**
* @param {Resolver} resolver the resolver
* @returns {void}
*/
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("AliasFieldPlugin", (request, resolveContext, callback) => {
if (!request.descriptionFileData) return callback();
const innerRequest = getInnerRequest(resolver, request);
if (!innerRequest) return callback();
// fileData 就是 npm 包的 package.json.browser 等字段对象:
// 形如:{ name: 'xxx', browser: 'lib/browser-only.js' } 替换掉模块的 main 整个入口模块
// 形如:{ name: 'xxx', browser: { './some-server-use.js': './some-client-use.js' } } 在浏览器环境下如果 require('xxx/some-server-use.js') 需要替换成 xxx/some-client-use.js 这个插件的工作就是干着的
const fieldData = DescriptionFileUtils.getField(
request.descriptionFileData,
this.field
);
if (fieldData === null || typeof fieldData !== "object") {
// 没有这个字段退出
if (resolveContext.log)
resolveContext.log(
"Field '" +
this.field +
"' doesn't contain a valid alias configuration"
);
return callback();
}
// 这个就是 Obj.hasOwnProperty()
const data = Object.prototype.hasOwnProperty.call(
fieldData,
innerRequest
)
? fieldData[innerRequest]
: innerRequest.startsWith("./") // 处理相对路径 './some-server-use.js' 变成 'some-server-use.js'
? fieldData[innerRequest.slice(2)]
: undefined; // 没找到
if (data === innerRequest) return callback();
if (data === undefined) return callback();
if (data === false) { // 屏蔽某些模块引用这个是规范的一部分
/** @type {ResolveRequest} */
const ignoreObj = {
...request,
path: false
};
if (typeof resolveContext.yield === "function") {
resolveContext.yield(ignoreObj);
return callback(null, null);
}
return callback(null, ignoreObj);
}
const obj = {
...request,
path: request.descriptionFileRoot,
request: data,
fullySpecified: false
};
resolver.doResolve(
target,
obj,
"aliased from description file " +
request.descriptionFilePath +
" with mapping '" +
innerRequest +
"' to '" +
data +
"'",
resolveContext,
(err, result) => {
if (err) return callback(err);
// Don't allow other aliasing or raw request
if (result === undefined) return callback(null, null);
callback(null, result);
}
);
});
}
};
三、总结
本文按照 ehanced-resolve 执行的流程注讲解其插件系统,这篇主要介绍了前 10 个插件:
- ParsePlugin:该插件用于解析原始的 request;
- DescriptionFilePlugin:读取 package.json 文件;
- NextPlugin:从一个 source 钩子推进到 target 钩子;
- AliasPlugin:处理 fallback、alias 等;
- AliasFieldPlugin:处理 package.json.browser 字段;