本文内容基于webpack 5.74.0
和enhanced-resolve 5.12.0
版本进行分析
webpack5核心流程
专栏共有5篇,使用流程图的形式分析了webpack5的构建原理
:
- 「Webpack5源码」make阶段(流程图)分析
- 「Webpack5源码」enhanced-resolve路径解析库源码分析
- 「Webpack5源码」seal阶段(流程图)分析(一)
- 「Webpack5源码」seal阶段分析(二)-SplitChunksPlugin源码
- 「Webpack5源码」seal阶段分析(三)-生成代码&runtime
由于enhanced-resolve
兼容了多种复杂情况的解析,想要将这些情况进行详细分析太耗费精力,因此本文只是尝试将所有流程进行浅显分析,通过本文,你可以对webpack
的resolve配置有一个整体的了解,如果对这方面想更加深入地研究,请结合其它文章进行阅读
本文是「Webpack5源码」make阶段(流程图)分析的补充文章,如果对webpack
流程不熟悉,请先看「Webpack5源码」make阶段(流程图)分析
文章内容
- 简要介绍
webpack
是如何使用enhanced-resolve
进行路径解析 - 分为三个流程图展示
enhanced-resolve
的解析流程,并为每一个流程图简单描述整体流程
整体流程分析
从上图可以知道,我们在webpack
解析过程中,会初始化this.getResolve("loader")
和this.getResolve("normal")
resolve一共分为两种配置,一种是文件类型的路径解析配置,一种是 loader 包的解析配置,比如上图中的内联url解析出来的到"babel-loader"
和"./index_item-line"
,"babel-loader"
会使用getResolve("loader")
进行解析,"./index_item-line"
会使用getResolve("normal")
进行解析
必须是内联url才会触发
resolveRequestArray()
和defaultResolve()
?是的,从源码上看只有解析到"-!"
、"!"
、!!"
才会使得unresolvedResource
和elements
两个数据不为空,才会触发resolveRequestArray()
和defaultResolve()
如果是
import _ from "loadsh"
会不会触发getResolve("loader")
进行解析?答案是不会的
type | normal | loader |
---|---|---|
resolveOptions |
其中normal
的参数获取是合并了webpack.config.js
和默认options
的结果
入口文件是EntryDependency
,它具有默认的category
="esm"
class EntryDependency extends ModuleDependency {
/**
* @param {string} request request path for entry
*/
constructor(request) {
super(request);
}
get type() {
return "entry";
}
get category() {
return "esm";
}
}
而在初始化normal
类型的Resolver
时,会触发hooks.resolveOptions
进行webpack.config.js
和一些默认参数的初始化
// node_modules/webpack/lib/ResolverFactory.js
_create(type, resolveOptionsWithDepType) {
/** @type {ResolveOptionsWithDependencyType} */
const originalResolveOptions = { ...resolveOptionsWithDepType };
const resolveOptionsTemp = this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType);
const resolveOptions = convertToResolveOptions(
resolveOptionsTemp
);
}
// node_modules/webpack/lib/WebpackOptionsApply.js
compiler.resolverFactory.hooks.resolveOptions
.for("normal")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
获取到的数据如下图所示
然后触发convertToResolveOptions()
方法,经历几个方法的调用执行后,最终触发resolveByProperty()
,如下图所示,会根据上面EntryDependency
得到的"esm"
进行参数的合并,最终得到完整的配置参数
然后使用resolver.resolve()
进行路径的解析
// node_modules/enhanced-resolve/lib/Resolver.js
resolve(context, path, request, resolveContext, callback) {
return this.doResolve(this.hooks.resolve, ...args);
}
// node_modules/enhanced-resolve/lib/Resolver.js
doResolve(hook, request, message, resolveContext, callback) {
const stackEntry = Resolver.createStackEntry(hook, request);
if (resolveContext.stack) {
newStack = new Set(resolveContext.stack);
newStack.add(stackEntry);
} else {
newStack = new Set([stackEntry]);
}
return hook.callAsync(request, innerContext, (err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
callback();
});
}
一开始调用doResolve()
时,我们传入的第一个参数hook
=this.hooks.resolve
,我们从上面代码块可以知道,会直接触发hook.callAsync
,也就是this.hooks.resolve.callAsync()
我们在初始化Resolver
时,会调用ResolverFactory.createResolver
,因此我们在调试这个流程时只要看这个方法即可,这个方法注册了很多Plugin
插件,每一个Plugin对其(多个)下家Plugin的引用而连接起来形成一条链。 请求在这个链上传递,如果符合其中一个下家Plugin
的条件,则以符合条件下家Plugin为基础,传递该请求(到下下家Plugin),直到某一条链走到底部
ResolverFactory.createResolver = function (options) {
const normalizedOptions = createOptions(options);
// resolve
for (const { source, resolveOptions } of [
{ source: "resolve", resolveOptions: { fullySpecified } },
{ source: "internal-resolve", resolveOptions: { fullySpecified: false } }
]) {
if (unsafeCache) {
plugins.push(
new UnsafeCachePlugin(
source,
cachePredicate,
unsafeCache,
cacheWithContext,
`new-${source}`
)
);
plugins.push(
new ParsePlugin(`new-${source}`, resolveOptions, "parsed-resolve")
);
} else {
plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));
}
}
}
每一个Plugin
都符合单一职责原则,比如有的Plugin
就是专门处理alias
问题,有的Plugin
处理文件后缀问题(添加.js后缀)
初始流程
ParsePlugin
核心代码就是使用resolver.parse(request.request)
方法,解析出目前路径所属的类型以及所携带的参数
其中fragment的定义可以参考URI's fragment,fragment
就是hash
值
// node_modules/enhanced-resolve/lib/Resolver.js
parse(identifier) {
const part = {
request: "",
query: "",
fragment: "",
module: false,
directory: false,
file: false,
internal: false
};
const parsedIdentifier = parseIdentifier(identifier);
if (!parsedIdentifier) return part;
[part.request, part.query, part.fragment] = parsedIdentifier;
if (part.request.length > 0) {
part.internal = this.isPrivate(identifier);
part.module = this.isModule(part.request);
part.directory = this.isDirectory(part.request);
if (part.directory) {
part.request = part.request.substr(0, part.request.length - 1);
}
}
return part;
}
其中request
、query
、fragment
的解析使用的是正则表达式匹配拿到对应的值
这正则表达式也太长了=_=
// node_modules/enhanced-resolve/lib/util/identifier.js
const PATH_QUERY_FRAGMENT_REGEXP = /^(#?(?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/;
function parseIdentifier(identifier) {
const match = PATH_QUERY_FRAGMENT_REGEXP.exec(identifier);
if (!match) return null;
return [
match[1].replace(/\0(.)/g, "$1"),
match[2] ? match[2].replace(/\0(.)/g, "$1") : "",
match[3] || ""
];
}
使用一个简单的示例可以帮助我们更好地理解正则表达式匹配出什么东西
至于这个正则表达式能否匹配出更加复杂的路径,请参考其它文章进行了解
DescriptionFilePlugin
const directory = this.pathIsFile
? DescriptionFileUtils.cdUp(path)
: path;
DescriptionFileUtils.loadDescriptionFile(
resolver,
directory,
this.filenames,
request.descriptionFilePath
? {
path: request.descriptionFilePath,
content: request.descriptionFileData,
directory: /** @type {string} */ (request.descriptionFileRoot)
}
: undefined,
resolveContext,
(err, result) => {
const relativePath =
"." + path.substr(result.directory.length).replace(/\\/g, "/");
const obj = {
...request,
descriptionFilePath: result.path,
descriptionFileData: result.content,
descriptionFileRoot: result.directory,
relativePath: relativePath
};
resolver.doResolve(target, obj, ...);
}
);
DescriptionFileUtils.cdUp(path)
: 获取最后一个"/"
的位置,然后使用path.substr(0, position)
,获取directory
比如传入path
="/Users/A/B/js-enhanced_resolve"
,获取到的是directory
="/Users/A/B"
DescriptionFileUtils.loadDescriptionFile
: 先使用directory
拼接this.filenames[i]
进行描述文件的查找,如果找不到,则将directory
往上一级变更,即从"/Users/A/B"
->"/Users/A"
,然后再拼接this.filenames[i]
进行描述文件的查找
this.filenames
的内容是什么呢?
初始化DescriptionFilePlugin
时会传入this.filenames
,具体的值为createOptions()
创建默认配置时的descriptionFiles
参数,默认为["package.json"]
function createOptions(options) {
return {
descriptionFiles: Array.from(
new Set(options.descriptionFiles || ["package.json"])
)
}
}
延续上面示例,我们通过该Plugin
可以得到的数据如下所示,其中descriptionFileData
是package.json
的具体内容
relativePath
="." + request.path.substr(descriptionFileRoot.length).replace(/\\/g, "/")
AliasPlugin
主要核心点如下图所示
innerRequest = request.request || request.path
item.name
是我们在webpack.config.js
中配置的别名alias
,比如在这个例子中,我们配置了alias
如下所示,因此我们能拿到item.name
="aliasTest"
// webpack.config.js
alias: {
aliasTest: resolve(__dirname, 'src/item'),
}
// src/entry1.js
import {getG} from "aliasTest/common_____g";
- 别名替换的规则是,我们的路径
innerRequest.startsWith("aliasTest")
,也就是我们路径的第一个单词必须是别名,才能正常匹配 - 然后就是替换逻辑,将
"aliasTest"
替换为我们在webpack.config.js
配置的路径,形成新的路径newRequestStr
- 最后替换我们的
obj
数据中对应的request
参数,然后进入下一个Plugin
AliasFieldPlugin
根据环境的不同,使用不同的配置,比如浏览器环境和node
环境
默认type
="normal"
会初始化aliasFields
=['browser']
type | normal | loader |
---|---|---|
resolveOptions |
如果webpack.config.js
配置了aliasFields
,则最终normal
类型的resolveOptions
会直接使用webpack.config.js
配置的aliasFields
// webpack.config.js
resolve: {
alias: {
aliasTest: resolve(__dirname, 'src/item'),
},
aliasFields: ['node222'],
}
同时我们还需要在package.json
中配置对应的aliasFields
属性,比如下面的代码,当我们找到文件路径为src/dir
时,我们就会替换为node_src/dir
// package.json
{
"node222": {
"src/dir": "node_src/dir"
}
}
AliasFieldPlugin
的apply()
方法解析如下,我们可以获取到一个文件路径为"src/dir"
,然后我们通过判断fieldData={"src/dir": "node_src/dir"}
是否满足fileldData[innerRequest]
触发替换规则
最终将替换成功后的data
覆盖obj.request
路径,原本的路径如上图所示是"src/dir"
,现在被替换为"node_src/dir"
,然后触发下一个Plugin
AliasFieldPlugin
这个属性个人理解为可以根据不同环境,比如根据browser
/node
环境分别进行文件路径替换,比如入口文件:browser
环境为browser/index.js
,node
环境为node/index.js
,详细用法请参考其它文章,本文不会做过多的分析
const obj = {
...request,
path: request.descriptionFileRoot,
request: data,
fullySpecified: false
};
resolver.doResolve(target, obj, ...)
ExtensionAliasPlugin
// webpack.config.js
module.exports = {
//...
resolve: {
extensionAlias: {
'.js': ['.ts', '.js'],
'.mjs': ['.mts', '.mjs'],
},
},
};
// node_modules/enhanced-resolve/lib/ResolverFactory.js
extensionAlias.forEach(item =>
plugins.push(
new ExtensionAliasPlugin("raw-resolve", item, "normal-resolve")
)
);
具体的apply()
代码如下所示,流程也比较简单
- 判断目前
requestPath.endsWith(extension)
是否成立,比如"./entry1.js"
满足配置的extensionAlias
中的".js"
,因此会命中继续下面的逻辑 - 判断拿到的
alias
类型,比如命中extension='.js'
,那么alias=['.ts', '.js']
,会使用forEachBail()
遍历尝试每一种情况替换是否满足提议 - 最终结果调用
stoppingCallback()
方法,比如"./entry1.js"
->"./entry1.ts"
,"./entry1.js"
->"./entry1.js"
,最终只有"./entry1.js"
->"./entry1.js"
满足提议,调用stoppingCallback()
方法
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) => {
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);
};
if (typeof alias === "string") {
resolve(alias, stoppingCallback);
} else if (alias.length > 1) {
forEachBail(alias, resolve, stoppingCallback);
} else {
resolve(alias[0], stoppingCallback);
}
});
模块处理
ConditionalPlugin
一种条件判断的Plugin
,全部代码如下所示,初始化会传入一个条件,判断是否满足这个条件,则可以继续下一个Plugin
for (const prop of keys) {
if (request[prop] !== test[prop]) return callback();
}
resolver.doResolve(target,request,...)
举一个例子,下面初始化时传入{ module: true }
,因此如果这个文件是module
类型,那么遇到这个ConditionalPlugin
则符合条件,那么它就会从"after-normal-resolve"
->"raw-module"
,ConditionalPlugin
起到一种根据处理类型不同从而跳转到不同Plugin
的目的
// node_modules/enhanced-resolve/lib/ResolverFactory.js
plugins.push(
new ConditionalPlugin(
"after-normal-resolve",
{ module: true },
"resolve as module",
false,
"raw-module"
)
);
plugins.push(
new ConditionalPlugin(
"after-normal-resolve",
{ internal: true },
"resolve as internal import",
false,
"internal"
)
);
RootsPlugin
// webpack.config.js
const fixtures = path.resolve(__dirname, 'fixtures');
module.exports = {
//...
resolve: {
roots: [__dirname, fixtures],
},
};
处理resolve.roots
参数,判断目前的请求路径是否以"/"
开头,如果是,则说明是根目录开头的文件,会将roots[i]
拼到文件路径上,形成新的path=roots[i]+request.request
// RootsPlugin apply
if (!req.startsWith("/")) return callback();
forEachBail(
this.roots,
(root, callback) => {
const path = resolver.join(root, req.slice(1));
const obj = {
...request,
path,
relativePath: request.relativePath && path
};
resolver.doResolve(
target,
obj,
`root path ${root}`,
resolveContext,
callback
);
},
callback
);
JoinRequestPlugin
改变path
、relativePath
、request
path
=path
+request
relativePath
=relativePath
+request
request
=undefined
本质是将目前请求的路径加上请求文件的名称,形成请求文件的绝对路径
const obj = {
...request,
path: resolver.join(request.path, request.request),
relativePath:
request.relativePath &&
resolver.join(request.relativePath, request.request),
request: undefined
};
resolver.doResolve(target, obj, null, resolveContext, callback);
举个例子,如下图所示,request.request
都并入了request.path
和request.relativePath
中
ImportsFieldPlugin(简单介绍)
具体配置可以参考webpack官方文档的resolve.importsFields描述
处理resolve.importsFields
字段
跟aliasFields
一样,需要在package.json
配置字段(配置字段名称跟webpack.config.js
声明一致),该字段用于提供包的内部请求(以# 开头的请求被视为内部请求)
感兴趣请参考其它文章,比如 Node最新Module导入导出规范,本文不会深入研究
下面的说明摘录自文章 Node最新Module导入导出规范
imports 字段中的入口必须是以 # 开头的字符串。
导入映射允许映射外部包。这个字段定义了当前包的子路径导入。
// webpack.config.js
module.exports = {
//...
resolve: {
//如果不手动配置下面,默认为['imports']
importsFields: ['browser', 'module', 'main'],
},
};
// package.json
{
"imports": {
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
}
}
SelfReferencePluginPlugin(简单介绍)
具体配置可以参考https://webpack.js.org/guides/package-exports
处理resolve.exportsFields
跟aliasFields
一样,需要在package.json
配置字段(配置字段名称跟webpack.config.js
声明一致),用于解析模块请求的字段
感兴趣请参考其它文章,比如 Node最新Module导入导出规范,本文不会深入研究
下面的说明摘录自文章 Node最新Module导入导出规范
"exports"
字段可以定义包的入口,而包则可以通过 node_modules
查找或自引用导入。Node.js 12+
开始支持 "exports"
,作为 "main"
的替代,它既支持定义子路径导出和条件导出,又封闭了内部未导出的模块。
条件导出也可以在 "exports"
中使用,以定义每个环境中不同的包入口,包括这个包是通过 require
还是通过 import
引入。
所有在 "exports"
中定义的路径必须是以 ./
开头的相对路径URL。
// webpack.config.js
module.exports = {
//...
resolve: {
//如果不手动配置下面,默认为['exports']
exportsFields: ['exports', 'myCompanyExports'],
},
};
// package.json
{
"exports": {
"./features/*": "./src/features/*.js"
}
}
ModulesInHierarchicalDirectoriesPlugin
source
和target
是上下Plugin
的连接,这个在每一个Plugin
都具备directories
默认为["node_modules"]
class ModulesInHierarchicalDirectoriesPlugin {
constructor(source, directories, target) {
this.source = source;
this.directories = /** @type {Array<string>} */ ([]).concat(directories);
this.target = target;
}
}
// apply()方法
const fs = resolver.fileSystem;
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;
}, []);
console.warn(addrs);
forEachBail(
addrs,
(addr, callback) => {
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
);
}
if (resolveContext.log)
resolveContext.log(
addr + " doesn't exist or is not a directory"
);
if (resolveContext.missingDependencies)
resolveContext.missingDependencies.add(addr);
return callback();
});
},
callback
);
如上面代码块所示,我们会使用getPaths()
获取所有node_modules
目录的可能性,如下图所示,我们会从path
逐级向上遍历,拼凑出paths
和segments
然后我们利用getPaths()
的paths
数据,拼凑出所有node_modules
可能的绝对路径(如下图addrs
所示),最终我们通过某一个绝对路径是否是目录,来获取其中某一个路径addr
,然后覆盖目前的obj
,形成第三方node_modules
库的绝对路径,如下图所示的obj.path
+obj.request
,就第三方库loadsh
的绝对路径
ModulesInRootPlugin
在初始化过程中,如果modules
的item
是数组类型,则触发上面分析的ModulesInHierarchicalDirectoriesPlugin
如果modules
的item
不是数组类型,则触发ModulesInRootPlugin
//node_modules/enhanced-resolve/lib/ResolverFactory.js
exports.createResolver = function (options) {
modules.forEach(item => {
if (Array.isArray(item)) {
//...
plugins.push(
new ModulesInHierarchicalDirectoriesPlugin(
"raw-module",
item,
"module"
)
);
} else {
plugins.push(new ModulesInRootPlugin("raw-module", item, "module"));
}
});
}
ModulesInRootPlugin
的apply
代码也比较粗暴简单,直接认为你传入的item=modules[i]
就是node_modules
的绝对路径目录,跟ModulesInHierarchicalDirectoriesPlugin
相比较,减少了所有可能性目录的寻找以及判断是否是目录stat.isDirectory()
的逻辑
const obj = {
...request,
path: this.path,
request: "./" + request.request,
module: false
};
resolver.doResolve(
target,
obj,
"looking for modules in " + this.path,
resolveContext,
callback
);
JoinRequestPartPlugin
检测import
的库是否存在多层目录,比如不是"vue"
,而是"vue/test"
,将path
拼接为原来的path
+前缀的"/vue"
,relativePath
拼接为原来的relativePath
+前缀目录名"/vue"
,request
拼接为路径的最后一部分,为"./test"
moduleName
就是第三方库的目录名称,remainingRequest
就是除去第三方库的目录名称的剩余部分
DirectoryExistsPlugin
直接判断request.path
是否是目录,是的话就继续下一个Plugin
// DirectoryExistsPlugin apply
const fs = resolver.fileSystem;
const directory = request.path;
if (!directory) return callback();
fs.stat(directory, (err, stat) => {
//...省略很多不是directory的提示逻辑
if (resolveContext.fileDependencies)
resolveContext.fileDependencies.add(directory);
resolver.doResolve(
target,
request,
`existing directory ${directory}`,
resolveContext,
callback
);
});
ExportsFieldPlugin(简单介绍)
与SelfReferencePluginPlugin
(简单介绍)的作用一致,本文不深入研究,请读者自行选择其它文章进行研究
具体配置可以参考https://webpack.js.org/guides/package-exports
处理resolve.exportsFields
跟aliasFields
一样,需要在package.json
配置字段(配置字段名称跟webpack.config.js
声明一致),用于解析模块请求的字段
目录/文件路径处理
UseFilePlugin
处理resolve.mainFiles
数据,如果不在webpack.config.js
中设置该参数,默认初始化为["index"]
// webpack.config.js
module.exports = {
//...
resolve: {
mainFiles: ['test'],
},
};
源码也非常简单,进行obj.path
=原来path
+mainFile
的拼接,进行obj.relativePath
=原来relativePath
+mainFile
的拼接,拼接后形成一个文件请求路径,然后进行文件类型的解析
// UseFilePlugin apply
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
);
举一个具体的例子如下图所示,我们请求的是一个import {dirIndex} from "./dir"
,这个链接我们是没有声明xx.js
文件的,UseFilePlugin
会自动帮我们补齐最后面的index
成为import {dirIndex} from "./dir"
UseFilePlugin
是目录类型的处理,也就是说如果具备了xxx.js
是不会走到目录这一条链路的,如下图所示,先进行了目录是否存在的判断,然后才进行index.js
的添加
MainFieldPlugin
处理resolve.mainFields
数据,如果不在webpack.config.js
中设置该参数,默认初始化为["main"]
注意!UseFilePlugin
处理的是resolve.mainFiles
!跟MainFieldPlugin
处理的resolve.mainFields
是不同名字的!
// webpack.config.js
module.exports = {
//...
resolve: {
mainFields: ['browser', 'module', 'main'],
},
};
跟aliasField
一样,同样需要在package.json
中配置对应的参数
比如bable-loader
的描述文件在xxxx/node_modules/babel-loader/package.json
babel-loader/package.json
中"main"
声明为"lib/index.js"
// package.json
{
"name": "babel-loader",
"version": "9.1.0",
"description": "babel module loader for webpack",
"files": [
"lib"
],
"main": "lib/index.js",
}
那如果没有在package.json
中声明"main"
所对应的值,比如下面main: ""
,那会发生什么?
// package.json
{
"name": "babel-loader",
"version": "9.1.0",
"description": "babel module loader for webpack",
"files": [
"lib"
],
"main": ""
}
那就会不走这个MainFieldPlugin
,而是直接走上面分析的UseFilePlugin
,直接在后面添加一个index.js
从UseFilePlugin
的分析可以知道,UseFilePlugin
和MainFieldPlugin
是并行的两个Plugin
AppendPlugin
上面几个Plugin
都是目录类型处理的Plugin
,现在开始文件类型处理的Plugin
// webpack.config.js
module.exports = {
//...
resolve: {
extensions: ['.ts', '.js', '.wasm'],
},
};
处理resolve.extensions
参数,如果不在webpack.config.js
中设置该参数,默认为[".js", ".json", ".node"]
,但是convertToResolveOptions()
方法由于"esm"
进行参数的合并,因此resolve.extensions
参数不为空,会取[".js", ".json", ".wasm"]
整体代码逻辑也非常简单,直接用一张示例图就可以明白,就是不断往目前的请求路径加后缀,看看哪一个符合就进入下一个阶段
FileExistsPlugin
直接判断request.path
是否存在该文件,是的话就继续下一个Plugin
const file = request.path;
if (!file) return callback();
fs.stat(file, (err, stat) => {
//...省略很多不是file的提示逻辑
if (resolveContext.fileDependencies)
resolveContext.fileDependencies.add(file);
resolver.doResolve(
target,
request,
"existing file: " + file,
resolveContext,
callback
);
});
SymlinkPlugin
resolve.symlinks
默认为true
,如果不配置webpack.config.js
为symlinks=false
,则默认开启symlinks=true
,启用后,符号链接资源将解析为它们的真实路径,而不是它们的符号链接位置
// webpack.config.js
module.exports = {
//...
resolve: {
symlinks: true,
},
};
resolve.symlinks
默认为true
,这个时候会检查是否存在软链情况(可以简单看作是window
系统的快捷方式),如果存在,则替换path
成为真实路径
// SymlinkPlugin apply
const pathsResult = getPaths(request.path);
const pathSegments = pathsResult.segments;
const paths = pathsResult.paths;
let containsSymlink = false;
let idx = -1;
forEachBail(
paths,
(path, callback) => {
idx++;
if (resolveContext.fileDependencies)
resolveContext.fileDependencies.add(path);
fs.readlink(path, (err, result) => {
if (!err && result) {
pathSegments[idx] = result;
containsSymlink = true;
// Shortcut when absolute symlink found
const resultType = getType(result.toString());
if (
resultType === PathType.AbsoluteWin ||
resultType === PathType.AbsolutePosix
) {
return callback(null, idx);
}
}
callback();
});
},
(err, idx) => {
if (!containsSymlink) return callback();
const resultSegments =
typeof idx === "number"
? pathSegments.slice(0, idx + 1)
: pathSegments.slice();
const result = resultSegments.reduceRight((a, b) => {
return resolver.join(a, b);
});
const obj = {
...request,
path: result
};
resolver.doResolve(
target,
obj,
"resolved symlink to " + result,
resolveContext,
callback
);
}
)
上面apply()
方法的代码虽然有点长,但是逻辑是非常简单的
使用示例
request.path='/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src/entry1.js'
我们可以得到
pathSegments=['entry1.js', 'src', 'js-enhanced_resolve', 'webpack-debugger',
'Frontend-Articles', 'blog', 'wcbbcc', 'Users', '/'];
paths=[
'/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src/entry1.js',
'/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src',
......
]
上面的forEachBail
循环中idx=0
时,path="/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src/entry1.js"
idx=1
时,path="/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src"
idx=2
时,path="/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve"
如果fs.readlink(path)
成功,能够正确获取到实际的链接result
时,我们会使用pathSegments[idx]
= result
比如idx=2
能够成功,那么
// pathSegments[2] = js-enhanced_resolve
// path的最后一个单词被覆盖为result
pathSegments[2] = fs.readlink(path)的结果
获取目前的resultSegments
=pathSegments.slice(0, idx + 1)
,即
resultSegments=['entry1.js', 'src', fs.readlink(path)的结果];
const result = resultSegments.reduceRight((a, b) => {
return resolver.join(a, b);
});
result = fs.readlink(path)的结果 + '/src/entry1.js';
const obj = {
...request,
path: result
};
resolver.doResolve(target, obj, ...);
ResultPlugin
最后一个Plugin
,返回结果!完成整个resolve
流程
const obj = { ...request };
if (resolverContext.log)
resolverContext.log("reporting result " + obj.path);
resolver.hooks.result.callAsync(obj, resolverContext, err => {
if (err) return callback(err);
if (typeof resolverContext.yield === "function") {
resolverContext.yield(obj);
callback(null, null);
} else {
callback(null, obj);
}
});
具体实例-普通目录解析
使用流程图展示所有Plugin
的流向以及对Plugin
的处理做简单的注释
具体实例-node_modules第三方模块解析
由于篇幅原因,这里不进行具体的流程图分析,将使用简单的文字描述展示整体流程
import _ from "loadsh";
// 第三方模块的resolve测试
console.error( _.add(3, 4));
初始模块resolve
ParsePlugin
: 初始化流程,使用resolver.parse(request.request)
方法,解析出目前路径所属的类型以及所携带的参数DescriptionFilePlugin
: 解析package.json
,获取package.json
对应的路径以及它本身的内容,后面需要读取里面的内容进行resolve
NextPlugin
: 由于alias
、aliasField
、extensionAlias
都不需要处理,因此直接跳到normal-resolve
状态
模块处理normal-resolve
ConditionalPlugin
: 触发module
=true
状态,进入raw-module
状态ModulesInHierarchicalDirectoriesPlugin
: 根据目前import vue form "vue"
所在的文件路径request.path
,一层一层不停往parent
目录找node_modules
的位置,最终拼凑出来path
=node_modules
的绝对路径以及request
="./vue"
JoinRequestPartPlugin
: 检测import
的库是否存在多层目录,比如不是"vue"
,而是"vue/test"
,将path
拼接为原来的path
+前缀的"/vue"
,relativePath
拼接为原来的relativePath
+前缀目录名"/vue"
,request
拼接为路径的最后一部分,为"./test"
尝试这个模块是否是单文件,尝试从
resolve-as-module
转化为undescribed-raw-file
(参考模块处理的流程图),即尝试是不是loadsh.js
这种模式,经过一圈Plugin
的调用,最终发现下面三种文件都不存在(参考AppendPlugin
的分析),无法从resolve-as-module
转化为undescribed-raw-file
/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh.js
/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh.json
/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh.wasm
DirectoryExistsPlugin
: 使用resolver.fileSystem.fs.stat
进行当前path
的判断,如果目录存在,则触发doResolve()
DescriptionFilePlugin
: 解析package.json
,获取package.json
对应的路径以及它本身的内容,后面需要读取里面的内容进行resolve
,package.json
从"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/package.json"
更改为"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh/package.json"
NextPlugin
: 由于不需要处理resolve.exportsField
字段,直接跳转到resolve-in-existing-directory
状态JoinRequestPlugin
: 改变path
、relativePath
、request
,即path
=path
+request
,relativePath
=relativePath
+request
,request
=undefined
,本质是将目前请求的路径加上请求文件的名称,形成请求文件的绝对路径
目录/文件路径处理relative
第1次relative
DescriptionFilePlugin
: 解析package.json
,获取package.json
对应的路径以及它本身的内容package.json
从"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh/package.json"
更改为"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/package.json"
relativePath
从"."
更改为"./node_modules/loadsh"
DirectoryExistsPlugin
: 使用resolver.fileSystem.fs.stat
进行当前path
的判断,如果目录存在,则触发doResolve()
,此时判断该目录loadsh
存在!DescriptionFilePlugin
: 解析package.json
,获取package.json
对应的路径以及它本身的内容package.json
从"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/package.json"
更改为"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh/package.json"
relativePath
从"./node_modules/loadsh"
更改为"."
MainFieldPlugin
: 处理resolve.mainFields
数据,在自己项目的package.json
中配置,用于指定从npm
包中导入模块时,此选项将决定在 (npm
包所在的)package.json
中使用哪个字段进行入口文件的指定
注意,lodash
这个第三方库的入口文件不是"index.js"
,而是"lodash.js"
,也就是"./node_modules/loadsh/lodash.js"
,通过resolve.mainFields
指定!!!!!此时obj.request
="./lodash.js"
JoinRequestPlugin
: 改变path
、relativePath
、request
,设置path
=path
+request
、relativePath
=relativePath
+request
、request
=undefined
,本质是将目前请求的路径加上请求文件的名称,形成请求文件的绝对路径
obj.request
并入到path
和relativePath
中,path
="/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh/lodash.js"
第2次relative
JoinRequestPlugin
的下一个状态又是relative
!但是跟第1次relative
相比较,数据已经从目录loadsh
->文件loadsh/lodash.js
,因此走的是跟第1次relative
不同的路
由于不需要加后缀以及没有别名的替换逻辑,直接跳转到FileExistsPlugin
(参考上面目录/文件路径处理的流程图即可明白)
FileExistsPlugin
: 使用resolver.fileSystem.fs.stat
进行当前path
的判断,如果路径存在,说明文件存在,则触发doResolve()
没有软链,直接跳过
SymlinkPlugin
ResultPlugin
: 结束Plugin
,返回结果
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。