一、前文回顾
上文按照 ehanced-resolve 执行的流程注讲解其插件系统,这篇主要介绍了前 10 个插件:
- 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;
最后我们还用 强盛集团 的例子讲了一下内部 request 的处理过程,这是全网最全的讲解了。再次声讨那些用机器翻译瞎TM翻译一通,例子不写一个就瞎写的博主!
这里我们继续 enhanced-resolve 插件的注册工作;
二、插件注册
接上文继续后面的插件注册!
2.11 SelfReferencePlugin
这个插件的作用并不算复杂,他的原理是结合 exportsFields 配置项和 package.json.exports 字段改写默认的主入口以及子路径的解析规则的:
相关实现如下:
const DescriptionFileUtils = require("./DescriptionFileUtils");
const slashCode = "/".charCodeAt(0);
module.exports = class SelfReferencePlugin {
constructor(source, fieldNamePath, target) {
this.source = source;
this.target = target;
this.fieldName = fieldNamePath;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("SelfReferencePlugin", (request, resolveContext, callback) => {
if (!request.descriptionFilePath) return callback();
const req = request.request;
if (!req) return callback();
// Feature is only enabled when an exports field is present
// 当 package.json.exports 字段存在时才启用该特性
// 获取 exports 字段对应的对象:
const exportsField = DescriptionFileUtils.getField(
request.descriptionFileData,
this.fieldName
);
if (!exportsField) return callback();
// 获取包的名字
const name = DescriptionFileUtils.getField(
request.descriptionFileData,
"name"
);
if (typeof name !== "string") return callback();
// 处理直接导入 包名 : import gao from 'qiang-sheng-group'
// 还有子路径: import tangxiaolong from 'qiang-sheng-group/tangxiaolong/some.js'
if (
req.startsWith(name) &&
(req.length === name.length || // 包名
req.charCodeAt(name.length) === slashCode) // 包名后面紧跟着 / 的就是子路径
) {
// 如果直接是包名:则 remainingRequest 就是 "."
// 如果是子路径:则变成 ./除了包名以外剩下的路径
const remainingRequest = `.${req.slice(name.length)}`;
const obj = {
...request,
request: remainingRequest,
path: /** @type {string} */ (request.descriptionFileRoot),
relativePath: "."
};
// 继续解析:
resolver.doResolve(
target,
obj,
"self reference",
resolveContext,
callback
);
} else {
return callback();
}
});
}
};
2.12 ModulesInHierarchicalDirectoriesPlugin
这个插件中文名 在有层级的目录中的模块插件,所谓层级就是 Node.js 中 require 查找算法一样,从当前目录查 node_modules,如果没找到就向上找,一直找到 / 目录下的 node_modules。
该插件的实现主要有以下步骤:
- 构造可能的查找路径。方式其实很简单,就是把 request.path 拆分成一段一段的,给各段拼接 node_modules,相当于查到到 / 下的 node_modules 目录。比如 request.path = /users/rmb/Document/w5-proj,addrs 如下:
/users/rmb/Document/w5-proj/node_modules/users/rmb/Document/node_modules/users/rmb/node_modules/users/node_modules/node_modules
- 遍历 addrs 数组,期间通过 fs.stat 检验这些目录是否存在;
- 目录不存在就是 missingDependencies;
const forEachBail = require("./forEachBail");
const getPaths = require("./getPaths");
module.exports = class ModulesInHierarchicalDirectoriesPlugin {
constructor(source, directories, target) {
this.source = source;
this.directories = ([]).concat(directories);
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync(
"ModulesInHierarchicalDirectoriesPlugin",
(request, resolveContext, callback) => {
const fs = resolver.fileSystem;
// 1. 构造可能的查找路径
const addrs = getPaths(request.path)
.paths.map(p => {
return this.directories.map(d => resolver.join(p, d));
})
.reduce((array, p) => {
array.push.apply(array, p);
return array;
}, []);
// 2. 遍历 addrs 数组,期间通过 fs.stat 校验这些目录是否存在
forEachBail(
addrs,
(addr, callback) => {
// fs.stat 验证包管理目录存在与否
fs.stat(addr, (err, stat) => {
// 存在且是目录:
if (!err && stat && stat.isDirectory()) {
const obj = {
...request,
path: addr,
request: "./" + request.request,
module: false
};
const message = "looking for modules in " + addr;
return resolver.doResolve(
target,
obj,
message,
resolveContext,
callback
);
}
// 3.
if (resolveContext.log)
resolveContext.log(
addr + " doesn't exist or is not a directory"
);
if (resolveContext.missingDependencies)
resolveContext.missingDependencies.add(addr);
return callback();
});
},
callback
);
}
);
}
};
2.13 PnpPlugin
这个玩意儿歇会儿吧😂,我没用过 pnp 。。。。等我学会了再来补,留个坑!! 如果有大神有链接,评论一下,我想这里引(白)用(嫖)一下
2.14 JoinRequestPartPlugin
每个高大上的插件都有一个朴素的实现😂,这个插件检查 @namespace 这种带有 @ 符号命名空间的 request 中是否带有2个 /,例如:@namespace/sub/sub2/module.js 这种。这个时候改写 path 为 @namespace/sub/sub2,改写 request ./module.js。
const namespaceStartCharCode = "@".charCodeAt(0);
module.exports = class JoinRequestPartPlugin {
constructor(source, target) {
this.source = source;
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync(
"JoinRequestPartPlugin",
(request, resolveContext, callback) => {
// request 有长这样的: ./@jridgewell/trace-mapping
const req = request.request || "";
let i = req.indexOf("/", 3); // 所以就是从 ./@ 后面查找第一个 /
if (i >= 0 && req.charCodeAt(2) === namespaceStartCharCode) { // @ 符号
// 如果是带命名空间的要看在命名空间的第一个 / 后面是不是还有 /
// 比如: ./@some-name/sug-module/sub2-m.js 这种,要找的是 sub2-m.js 前面的 /
i = req.indexOf("/", i + 1);
}
let moduleName, remainingRequest, fullySpecified;
if (i < 0) {
// 没有第二个斜杠,都是 @namespace/ 的第一级子模块
moduleName = req;
remainingRequest = "."; // . 表示当前
fullySpecified = false;
} else {
// 有第二个斜杠,说名是 @namespace/sub1 的子模块,也就是 @namespace 的孙子模块
moduleName = req.slice(0, i); // 改写 moduleName 到第二个斜杠 ,@namespace/sub1 这个
remainingRequest = "." + req.slice(i); // reaminingRequest 变成 ./sub2-module.js
fullySpecified = request.fullySpecified;
}
// 重新构造 path、relativePath、request
const obj = {
...request,
path: resolver.join(request.path, moduleName),
relativePath:
request.relativePath &&
resolver.join(request.relativePath, moduleName),
request: remainingRequest,
fullySpecified
};
resolver.doResolve(target, obj, null, resolveContext, callback);
}
);
}
};
2.15 DirectoryExistsPlugin
这个插件的作用就更朴素了,主要是判断截止到当前流程,path 是不是一个真实存在的目录。
用 fs.stat 检测 request.path(下称 directory),如果 fs.stat 异常或者 fs.stat 结果显示不是文件夹(目录,stat.isDirectory() 返回 false),则把这个目录加入到 resolveContext.missingDependencies 中。否则,就把 directory 加入到 resolveContext.fileDependencies 中。
module.exports = class DirectoryExistsPlugin {
constructor(source, target) {
this.source = source;
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync(
"DirectoryExistsPlugin",
(request, resolveContext, callback) => {
const fs = resolver.fileSystem;
const directory = request.path;
// 判断 path 是否为 falsy 值,如果为假值就返回
if (!directory) return callback();
// fs.stat 验证路径
fs.stat(directory, (err, stat) => {
if (err || !stat) {
// 如果有错或者没得到 stat 对象说明有问题
// 把这个路径加入到 resolveContext.missingDependencies 中
if (resolveContext.missingDependencies)
resolveContext.missingDependencies.add(directory);
if (resolveContext.log)
resolveContext.log(directory + " doesn't exist");
return callback();
}
if (!stat.isDirectory()) {
// directory 路径不是个目录(比如是文件)
// 把这个路径加入到 resolveContext.missingDependencies 中
if (resolveContext.missingDependencies)
resolveContext.missingDependencies.add(directory);
if (resolveContext.log)
resolveContext.log(directory + " is not a directory");
return callback();
}
// 如果 resolveContext.fileDependencies 属性存在则收集当前目录
if (resolveContext.fileDependencies)
resolveContext.fileDependencies.add(directory);
// 执行后续解析流程
resolver.doResolve(
target,
request,
`existing directory ${directory}`,
resolveContext,
callback
);
});
}
);
}
};
2.16 ExportsFieldPlugin
这个插件是解析 package.json.exports 字段的,package.json.exports 字段是用于重新定义模块的导出规则的。除了可以覆盖默认的 main 字段,还可以通过重写实现部分子目录的模块导出重定向。
const path = require("path");
const DescriptionFileUtils = require("./DescriptionFileUtils");
const forEachBail = require("./forEachBail");
const { processExportsField } = require("./util/entrypoints");
const { parseIdentifier } = require("./util/identifier");
const { checkImportsExportsFieldTarget } = require("./util/path");
module.exports = class ExportsFieldPlugin {
constructor(source, conditionNames, fieldNamePath, target) {
this.source = source;
this.target = target;
this.conditionNames = conditionNames;
this.fieldName = fieldNamePath;
/** @type {WeakMap<any, FieldProcessor>} */
this.fieldProcessorCache = new WeakMap();
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("ExportsFieldPlugin", (request, resolveContext, callback) => {
// When there is no description file, abort
// 没有 package.json 跳过
if (!request.descriptionFilePath) return callback();
if (
// When the description file is inherited from parent, abort
// (There is no description file inside of this package)
// 如果 package.json 是从父目录继承的,跳过
request.relativePath !== "." ||
request.request === undefined
)
return callback();
const remainingRequest =
request.query || request.fragment
? (request.request === "." ? "./" : request.request) +
request.query +
request.fragment
: request.request;
/** @type {ExportsField|null} */
// 获取 package.json.exports 字段值
const exportsField = DescriptionFileUtils.getField(
request.descriptionFileData,
this.fieldName
);
if (!exportsField) return callback();
if (request.directory) {
return callback(
new Error(
`Resolving to directories is not possible with the exports field (request was ${remainingRequest}/)`
)
);
}
let paths;
try {
let fieldProcessor = this.fieldProcessorCache.get(
request.descriptionFileData
);
if (fieldProcessor === undefined) {
// 把 package.json 指定的 exports 对象处理成 fileProcessor 函数
fieldProcessor = processExportsField(exportsField);
this.fieldProcessorCache.set(
request.descriptionFileData,
fieldProcessor
);
}
// 利用根据 request 和 条件得到最终的路径数组
paths = fieldProcessor(remainingRequest, this.conditionNames);
} catch (err) {
if (resolveContext.log) {
resolveContext.log(
`Exports field in ${request.descriptionFilePath} can't be processed: ${err}`
);
}
return callback(err);
}
if (paths.length === 0) {
return callback(
new Error(
`Package path ${remainingRequest} is not exported from package ${request.descriptionFileRoot} (see exports field in ${request.descriptionFilePath})`
)
);
}
// 遍历 path 尝试
forEachBail(
paths,
(p, callback) => {
const parsedIdentifier = parseIdentifier(p);
if (!parsedIdentifier) return callback();
const [relativePath, query, fragment] = parsedIdentifier;
const error = checkImportsExportsFieldTarget(relativePath);
if (error) {
return callback(error);
}
const obj = {
...request,
request: undefined,
path: path.join(
/** @type {string} */ (request.descriptionFileRoot), // 拼接当前包的 package.json 和模块相对路径 就是解析出来的绝对路径
relativePath
),
relativePath,
query,
fragment
};
resolver.doResolve(
target,
obj,
"using exports field: " + p,
resolveContext,
callback
);
},
(err, result) => callback(err, result || null)
);
});
}
};
2.17 UseFilePlugin
处理导入目录时自动解析到目录下的 index.js;
module.exports = class UseFilePlugin {
constructor(source, filename, target) {
this.source = source;
this.filename = filename;
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("UseFilePlugin", (request, resolveContext, callback) => {
// 处理导入目录时自动解析到目录下的 index.js;
// resolve.mainFiles: ['index.js']
// 构造文件名:
const filePath = resolver.join(request.path, this.filename);
const obj = {
...request,
path: filePath,
relativePath:
request.relativePath &&
resolver.join(request.relativePath, this.filename)
};
resolver.doResolve(
target,
obj,
"using path: " + filePath,
resolveContext,
callback
);
});
}
};
2.18 MainFieldPlugin
这个插件的作用是把包名和 package.json.main 字段解析拼接,得到具体的主入口路径,解析的基础就是这个包的 package.json 所在的路径。
const path = require("path");
const DescriptionFileUtils = require("./DescriptionFileUtils");
const alreadyTriedMainField = Symbol("alreadyTriedMainField");
module.exports = class MainFieldPlugin {
constructor(source, options, target) {
this.source = source;
this.options = options;
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("MainFieldPlugin", (request, resolveContext, callback) => {
if (
request.path !== request.descriptionFileRoot ||
request[alreadyTriedMainField] === request.descriptionFilePath ||
!request.descriptionFilePath
)
return callback();
// package.json
const filename = path.basename(request.descriptionFilePath);
// package.json.main 字段值
let mainModule = DescriptionFileUtils.getField(
request.descriptionFileData,
this.options.name
);
if (
!mainModule ||
typeof mainModule !== "string" ||
mainModule === "." ||
mainModule === "./"
) {
// 不处理
return callback();
}
if (this.options.forceRelative && !/^..?\//.test(mainModule))
mainModule = "./" + mainModule;
const obj = {
...request,
request: mainModule, // 重写 request 为 包名/主入口模块
module: false,
directory: mainModule.endsWith("/"), // 是否为目录
[alreadyTriedMainField]: request.descriptionFilePath
};
return resolver.doResolve(
target,
obj,
"use " +
mainModule +
" from " +
this.options.name +
" in " +
filename,
resolveContext,
callback
);
});
}
};
三、总结
本文主要接上文介绍了 enhance-resolve 中实现 resolve 功能的内部插件实现细节,本小结主要讨论了以下插件:
- SelfReferencePlugin:其作用是结合 exportsFields 配置项和 package.json.exports 字段改写默认的主入口以及子路径的解析规则;
- ModulesInHierarchicalDirectoriesPlugin:其作用是模拟 require.resolve 行为,从当前目录查找 node_moduels 一直到 / 下的这一行为;
- PnpPlugin:咱也不知道,也不想学(一言不合就开摆,就问你气不气😂)
- JoinRequestPartPlugin:将通过命名空间引用其包内部模块的
@ns/sub/module 语法变成相对路径; - DirectoryExistsPlugin:主要是判断截止到当前流程,path 是不是一个真实存在的目录;
- ExportsFieldPlugin:主要实现的是 package.json 中的 exports 字段相关能力的插件;
- UseFilePlugin:实现导入写只写到目录的情况下自动解析到该目录下的 index.js 能力;
- MainFieldPlugin:实现 package.json 中 main 字段相关能力的插件;