本文内容基于
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.jscompiler.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.jsresolve(context, path, request, resolveContext, callback) { return this.doResolve(this.hooks.resolve, ...args);}// node_modules/enhanced-resolve/lib/Resolver.jsdoResolve(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.jsparse(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.jsconst 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.jsalias: { aliasTest: resolve(__dirname, 'src/item'),}// src/entry1.jsimport {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.jsresolve: { 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.jsmodule.exports = { //... resolve: { extensionAlias: { '.js': ['.ts', '.js'], '.mjs': ['.mts', '.mjs'], }, },};// node_modules/enhanced-resolve/lib/ResolverFactory.jsextensionAlias.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.jsplugins.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.jsconst fixtures = path.resolve(__dirname, 'fixtures');module.exports = { //... resolve: { roots: [__dirname, fixtures], },};
解决resolve.roots
参数,判断目前的申请门路是否以"/"
结尾,如果是,则阐明是根目录结尾的文件,会将roots[i]
拼到文件门路上,造成新的path=roots[i]+request.request
// RootsPlugin applyif (!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.jsmodule.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.jsmodule.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.jsexports.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 applyconst 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.jsmodule.exports = { //... resolve: { mainFiles: ['test'], },};
源码也非常简单,进行obj.path
=原来path
+mainFile
的拼接,进行obj.relativePath
=原来relativePath
+mainFile
的拼接,拼接后造成一个文件申请门路,而后进行文件类型的解析
// UseFilePlugin applyconst 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.jsmodule.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.jsmodule.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.jsmodule.exports = { //... resolve: { symlinks: true, },};
resolve.symlinks
默认为true
,这个时候会查看是否存在软链状况(能够简略看作是window
零碎的快捷方式),如果存在,则替换path
成为实在门路
// SymlinkPlugin applyconst 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的最初一个单词被笼罩为resultpathSegments[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
,返回后果