涨薪面技:写个 enhanced-resolve 插件(9)
一、前文回顾
上文按照 ehanced-resolve 执行的流程注讲解其插件系统,这篇主要介绍了前 10 个插件:
- ParsePlugin:该插件用于解析原始的 request;
- DescriptionFilePlugin:读取 package.json 文件;
- NextPlugin:从一个 source 钩子推进到 target 钩子;
- AliasPlugin:处理 fallback、alias 等;
- AliasFieldPlugin:处理 package.json.browser 字段;
二、流水线注册的插件(2)
2.1 ExtensionAliasPlguin
该插件的实现很简单,改写原有的 request,将其中的扩展名部分改写成配置的扩展名进行尝试。
const forEachBail = require("./forEachBail");
module.exports = class ExtensionAliasPlugin {
constructor(source, options, target) {
this.source = source;
this.options = options;
this.target = target;
}
/**
* @param {Resolver} resolver the resolver
* @returns {void}
*/
apply(resolver) {
const target = resolver.ensureHook(this.target);
const { extension, alias } = this.options;
resolver
.getHook(this.source)
.tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => {
const requestPath = request.request;
if (!requestPath || !requestPath.endsWith(extension)) return callback();
const resolve = (alias, callback) => {
// 改写 request,将原有的扩展名替换掉,重新执行 resolve
resolver.doResolve(
target,
{
...request,
request: `${requestPath.slice(0, -extension.length)}${alias}`, // 改写原理
fullySpecified: true
},
`aliased from extension alias with mapping '${extension}' to '${alias}'`,
resolveContext,
callback
);
};
// 停止回调函数
const stoppingCallback = (err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
// Don't allow other aliasing or raw request
return callback(null, null);
};
// 根据 alias 配置不同情况处理
// 1. alias 配置是一个字符串 extension: '.js', alias: '.ts'
if (typeof alias === "string") {
resolve(alias, stoppingCallback);
} else if (alias.length > 1) {
// 2. alias 配置一个数组且长度大于1: extension '.js' alias: ['.jsx', '.tsx', '.mjs']
forEachBail(alias, resolve, stoppingCallback);
} else {
// 3. alias 配置一个数组长度<= 1 的情况:extension '.js' alias: ['.jsx']
resolve(alias[0], stoppingCallback);
}
});
}
};
2.2 JoinRequestPlugin
拼接请求,把 request.path 和 request 拼接,相当于重新设定了 path,然后如果有 relativePath 同样拼接,还要置空原始 request。最后执行目标钩子。
module.exports = class JoinRequestPlugin {
constructor(source, target) {
this.source = source;
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("JoinRequestPlugin", (request, resolveContext, callback) => {
const obj = {
...request,
path: resolver.join(request.path, request.request), // request.path 是最终 resolver.resolve() 方法的路径解析结果
relativePath: // 如果有 relativePath 就改写一次拼接当前的 request
request.relativePath &&
resolver.join(request.relativePath, request.request),
request: undefined // 置空原始 request
};
resolver.doResolve(target, obj, null, resolveContext, callback);
});
}
};
2.3 ConditionalPlugin
判断本次 request 的类型是否满足条件(test),目前已有的匹配条件(test):
- { moduel: true }
- { internal: true }
{ directory: false, request: "." }
module.exports = class ConditionalPlugin {
constructor(source, test, message, allowAlternatives, target) {
this.source = source;
this.test = test;
this.message = message;
this.allowAlternatives = allowAlternatives;
this.target = target;
}
apply(resolver) {
const target = resolver.ensureHook(this.target);
const { test, message, allowAlternatives } = this;
const keys = Object.keys(test);
resolver
.getHook(this.source)
.tapAsync("ConditionalPlugin", (request, resolveContext, callback) => {
// test { module: true }
// test { internal: true }
for (const prop of keys) {
// 不满足条件就不引导到目标钩子
// 通过这种方式作为条件判断吗
if (request[prop] !== test[prop]) return callback();
}
resolver.doResolve(
target,
request,
message,
resolveContext,
allowAlternatives
? callback
: (err, result) => {
if (err) return callback(err);
// Don't allow other alternatives
if (result === undefined) return callback(null, null);
callback(null, result);
}
);
});
}
};
2.4 RootsPlugin
替换 obj 中的 path,是把 '/some/module.js' 中的第一个 /(表示根目录) 改写成 resolve.roots 指向的目录,相当于以 resolve.roots 作为根目录查找。
const forEachBail = require("./forEachBail");
class RootsPlugin {
constructor(source, roots, target) {
this.roots = Array.from(roots);
this.source = source;
this.target = target;
}
/**
* @param {Resolver} resolver the resolver
* @returns {void}
*/
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("RootsPlugin", (request, resolveContext, callback) => {
const req = request.request;
if (!req) return callback();
if (!req.startsWith("/")) return callback();
forEachBail(
this.roots,
(root, callback) => {
const path = resolver.join(root, req.slice(1)); // 把第一个 / 替换成 root 对应的路径;
const obj = {
...request,
path, // path 是最终解析结果
relativePath: request.relativePath && path
};
resolver.doResolve(
target,
obj,
`root path ${root}`,
resolveContext,
callback
);
},
callback
);
});
}
}
module.exports = RootsPlugin;
2.5 ImportsFieldPlugin
这个插件用于处理包的内部 request 的,听着很复杂,其实干了一件很简单的事儿: 首先说内部请求:#anyKey 就是一个内部请求;在某些包的 package.json 定义了 imports 字段,值是个对象,这个对象中的 key 就是以 # 开头的字符串,你可以认为这就某个模块的别名,只不过这个模块只能在这个包内部以 #anyKey 的形式导入。
而这个插件的作用就是把以 # 开头的内部 request,替换成 package.json.imports[#anyKey]
对应的真实路径然后去解析。
鉴于全网搜了半天没有找到一个靠谱的例子,我自己鼓捣写了一个(这个特性没啥用,反正我的 node_modules 下面没有搜到一个用过这玩意儿的)。。。。
不过也得吐槽一下全网用机翻写webpack 相关博客的人,瞎JB翻译一通就发表,连个例子都不写
2.10.1 内部请求示例
- webpack.config.js
resolve: {
importsFields: ['imports']
},
- src/index.js
import g from 'qiang-sheng-group'
console.log(g);
- node_modules/qiang-sheng-group 包的文件树:
.
├── lib
│ ├── gaoqiqiang.js
│ ├── tangxiaohu.js
│ └── tangxiaolong.js
└── package.json
- node_modules/qiang-sheng-group/package.json 文件
{
"name": "qiang-sheng-group",
"version": "1.0.0",
"description": "我怕风浪大?风浪越大,鱼越贵!",
"main": "gaoqiqiang.js",
"scripts": {
"test": "anxin"
},
"imports": {
"#xiaolong": "./lib/tangxiaolong.js", // imports 字段定义
"#xiaohu": "./lib/tangxiaohu.js"
},
"keywords": [
"see",
"you",
"again"
],
"author": "Mr.Gao",
"license": "ISC"
}
- node_modules/qiang-sheng-group/lib/gaoqiqiang.js 采用
#xiaolong
调用内部模块:
import Xiaolong from '#xiaolong'; // 这会发起一个 内部 request
console.log(Xiaolong)
export default {
ok: '就让这大风吹,大风吹~'
}
2.10.2 ImportsPlguin 实现细节
这是 debugger 运行中实例的 importsField,可以看到和上面 package.json.imports 是对应的:
插件代码实现:
const path = require("path");
const DescriptionFileUtils = require("./DescriptionFileUtils");
const forEachBail = require("./forEachBail");
const { processImportsField } = require("./util/entrypoints");
const { parseIdentifier } = require("./util/identifier");
const { checkImportsExportsFieldTarget } = require("./util/path");
const dotCode = ".".charCodeAt(0);
module.exports = class ImportsFieldPlugin {
constructor(
source,
conditionNames,
fieldNamePath,
targetFile,
targetPackage
) {
this.source = source;
this.targetFile = targetFile;
this.targetPackage = targetPackage;
this.conditionNames = conditionNames;
this.fieldName = fieldNamePath;
/** @type {WeakMap<any, FieldProcessor>} */
this.fieldProcessorCache = new WeakMap();
}
apply(resolver) {
const targetFile = resolver.ensureHook(this.targetFile);
const targetPackage = resolver.ensureHook(this.targetPackage);
resolver
.getHook(this.source)
.tapAsync("ImportsFieldPlugin", (request, resolveContext, callback) => {
// When there is no description file, abort
if (!request.descriptionFilePath || request.request === undefined) {
return callback();
}
// 获取内部请求 例如 gaoqiqiang.js 中的 #xiaolong
const remainingRequest =
request.request + request.query + request.fragment;
// 获取 package.json.imports 字段对应的映射对象 { "#xiaolong": "./lib/tangxiaolong.js", .... }
const importsField = DescriptionFileUtils.getField(
request.descriptionFileData,
this.fieldName
);
if (!importsField) return callback();
if (request.directory) {
return callback(new Error());
}
let paths;
try {
let fieldProcessor = this.fieldProcessorCache.get(
request.descriptionFileData
);
if (fieldProcessor === undefined) {
fieldProcessor = processImportsField(importsField);
this.fieldProcessorCache.set(
request.descriptionFileData,
fieldProcessor
);
}
// 调用 filedProcessor 这个字段处理函数,
// 结合 remainingRequest 和 this.conditionNames 获取到这个 internal request 对应的路径数组: ["./lib/tangxiaolong.js"]
paths = fieldProcessor(remainingRequest, this.conditionNames);
} catch (err) {
if (resolveContext.log) {
resolveContext.log(
`Imports field in ${request.descriptionFilePath} can't be processed: ${err}`
);
}
return callback(err);
}
if (paths.length === 0) {
return callback(new Error());
}
// 遍历得到的 path 数组
forEachBail(
paths,
(p, callback) => {
const parsedIdentifier = parseIdentifier(p);
if (!parsedIdentifier) return callback();
const [path_, query, fragment] = parsedIdentifier;
const error = checkImportsExportsFieldTarget(path_);
if (error) {
return callback(error);
}
// 根据路径的开头的第一个字符决定
switch (path_.charCodeAt(0)) {
// should be relative
case dotCode: {
// 如果以 . 开头当成相对路径解析
const obj = {
...request,
request: undefined,
path: path.join(
/** @type {string} */ (request.descriptionFileRoot),
path_
),
relativePath: path_,
query,
fragment
};
resolver.doResolve(
targetFile,
obj,
"using imports field: " + p,
resolveContext,
callback
);
break;
}
// package resolving 否则当做包解析
default: {
const obj = {
...request,
request: path_,
relativePath: path_,
fullySpecified: true,
query,
fragment
};
resolver.doResolve(
targetPackage,
obj,
"using imports field: " + p,
resolveContext,
callback
);
}
}
},
(err, result) => callback(err, result || null)
);
});
}
};
三、总结
本文接着上文详细描述了包括 ExtensionAliasPlguin、JoinRequestPlugin、ConditionalPlugin、RootsPlugin 和 ImportsFieldPlugin 插件的具体工作原理,以下为各个插件的作用:
- ExtensionAliasPlguin:拼接各个扩展名并尝试解析;
- JoinRequestPlugin:拼接请求,把 request.path 和 request 拼接;
- ConditionalPlugin:判断本次 request 的类型是否满足条件;
- RootsPlugin: 把的第一个 /(表示根目录) 改写成 resolve.roots 设定的目录;
- ImportsFieldPlugin:这个插件用于处理包的内部 request;
最后我们还用
强盛集团
的例子讲了一下内部 request 的处理过程,这是全网最全的讲解了;
到这里,有关 enhanced-resolve 这个库的流水线部分注册的所有内部插件的细节部分就完结了,这部分内容相当抽象,调试过程也不是很顺利,希望各位看官有耐心啊~
转载自:https://juejin.cn/post/7366527815292256290