共计 23168 个字符,预计需要花费 58 分钟才能阅读完成。
本文内容基于
webpack 5.74.0
和enhanced-resolve 5.12.0
版本进行剖析因为
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
,返回后果