Webpack源码解析
// 使用webpack版本
"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
打包主流程分析
上一篇文章 compiler.run 做了什么 讲到了文件的 resolve,下面我们就看看一个普通文件的resolve流程。
普通文件的 doResolve
核心就是组装 stack,然后进行钩子调用,每次调用钩子都不同,上一个流程会提供下一个流程的 hook,hook 上所有 plugin 都会在node_modules/enhanced-resolve/lib/ResolverFactory.js中实例化,实例化时传入 target,调用插件时(执行 plugin 实例的 apply 方法)会通过调用 const target = resolver.ensureHook(this.target); 挂载钩子(node_modules/enhanced-resolve/lib/Resolver.js 第 87 行)。
// node_modules/enhanced-resolve/lib/Resolver.js
return hook.callAsync(request, innerContext, (err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
callback();
});
1)第一个钩子是类 Resolver 挂载的 resolve 钩子,上面注册的钩子函数 UnsafeCachePlugin 查看是否存在 cacheEntry,存在的话直接返回缓存,不存在就 resolver.doResolve 继续回去执行 doResolve:
// node_modules/enhanced-resolve/lib/UnsafeCachePlugin.js
apply(resolver) {
// 挂载新的钩子 newResolve
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => {
if (!this.filterPredicate(request)) return callback();
// 查看是否有缓存
// cacheId --> '{"context":"","path":"/Users/***/lagou-edu/webpack-entry","request":"./src/index.js"}'
const cacheId = getCacheId(request, this.withContext);
const cacheEntry = this.cache[cacheId];
// 有缓存就执行回调,将缓存数据交由回调处理
if (cacheEntry) {
return callback(null, cacheEntry);
}
// 否则继续回去执行doResolve,只不过此时的hook变成了newResolve
resolver.doResolve(
target,
request,
null,
resolveContext,
(err, result) => {
if (err) return callback(err);
if (result) return callback(null, (this.cache[cacheId] = result));
callback();
}
);
});
}
此时 stack 中就存在一个内容:
0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
2)然后就是 UnsafeCachePlugin 挂载的第二个钩子 newResolve, 上面注册的钩子函数 ParsePlugin,解析文件类型:
// node_modules/enhanced-resolve/lib/ParsePlugin.js
apply(resolver) {
// 挂载钩子 parsedResolve
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("ParsePlugin", (request, resolveContext, callback) => {
// 解析文件类别
// { request: './src/index.js', query: '', module: false, directory: false, file: false }
const parsed = resolver.parse(request.request);
const obj = Object.assign({}, request, parsed);
// 合并参数
if (request.query && !parsed.query) {
obj.query = request.query;
}
// 如果打印日志的话就打印日志
if (parsed && resolveContext.log) {
if (parsed.module) resolveContext.log("Parsed request is a module");
if (parsed.directory)
resolveContext.log("Parsed request is a directory");
}
// 然后继续执行doResolve,此时的hook成为了parsedResolve
resolver.doResolve(target, obj, null, resolveContext, callback);
});
}
ParsePlugin 执行完毕会得到文件的类型,模块、文件、文件夹,和加载文件额外的请求参数,最终将信息合并到 request 中继续执行 doResolve,此时的 hook 变成了 parsedResolve。
此时 stack 中有两个内容:
0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
3)第三个钩子是 ParsePlugin 挂载的 parsedResolve, 上面注册的函数 DescriptionFilePlugin 和 NextPlugin:
DescriptionFilePlugin 是查找项目描述文件 package.json:
// node_modules/enhanced-resolve/lib/DescriptionFilePlugin.js
apply(resolver) {
// 挂载钩子 describedResolve
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("DescriptionFilePlugin", (request, resolveContext, callback) => {
// 获取问价夹路径(项目目录)
const directory = request.path;
// 加载描述文件package.json
DescriptionFileUtils.loadDescriptionFile(
resolver,
directory,
this.filenames,
resolveContext,
(err, result) => {
// 加载出错返回错误信息
if (err) return callback(err);
// 没有结果的话执行_nextPlugin
if (!result) {
if (resolveContext.missing) {
this.filenames.forEach((filename) => {
resolveContext.missing.add(resolver.join(directory, filename));
});
}
if (resolveContext.log)
resolveContext.log("No description file found");
return callback();
}
// 有结果的话将读取到 package.json 的信息和其所在的目录/路径信息,存入 request 中
const relativePath =
"." +
request.path.substr(result.directory.length).replace(/\\/g, "/");
const obj = Object.assign({}, request, {
descriptionFilePath: result.path,
descriptionFileData: result.content,
descriptionFileRoot: result.directory,
relativePath: relativePath,
});
// 继续执行事件流
resolver.doResolve(
target,
obj,
"using description file: " +
result.path +
" (relative path: " +
relativePath +
")",
resolveContext,
(err, result) => {
if (err) return callback(err);
// Don't allow other processing
if (result === undefined) return callback(null, null);
callback(null, result);
}
);
}
);
});
}
这里执行稍微复杂点,下面给出调用栈中的执行函数:
// VM46947652
(function anonymous(request, resolveContext, _callback) {
"use strict";
var _context;
var _x = this._x;
function _next0() {
// 取出_nextPlugin执行
var _fn1 = _x[1];
_fn1(request, resolveContext, (_err1, _result1) => {
if (_err1) {
_callback(_err1);
} else {
if (_result1 !== undefined) {
_callback(null, _result1);
} else {
_callback();
}
}
});
}
// DescriptionFilePlugin 注册的函数
var _fn0 = _x[0];
// 先执行读取
_fn0(request, resolveContext, (_err0, _result0) => {
// 存在错误直接调用钩子的回调,返回错误
if (_err0) {
_callback(_err0);
} else {
// 如果有读取结果,将结果返回
if (_result0 !== undefined) {
_callback(null, _result0);
} else {
// 否则就调用_next0
_next0();
}
}
});
});
NextPlugin 起到衔接的作用,内部直接调用 doResolve,触发下一个事件。当 DescriptionFilePlugin 中未找到 package.json 文件时,会进入 NextPlugin,然后让事件流继续下去。
这一步的目的主要是为了下一步的路径别名解析做准备,因为路径别名依赖于 package.json。
如果 DescriptionFileUtils.loadDescriptionFile 调试不成功,反正我是没成功过(调试了无数遍,不知道原因,无法正常进入回调),在内部直接改成 resolver.doResolve(target, request, null, resolveContext, callback);,进行下一个步骤继续调试。
此时 stack 就有三个内容了:
0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
4)第四个钩子是 DescriptionFilePlugin 挂载的 describedResolve,这个钩子上注册了 44 个函数:
AliasFieldPlugin --> 40 个 AliasPlugin --> ModuleKindPlugin --> JoinRequestPlugin --> RootPlugin
// node_modules/enhanced-resolve/lib/AliasFieldPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// target 挂载为resolve,替换完就会进行resolve,进入一个新文件、模块的加载解析
resolver
.getHook(this.source)
.tapAsync("AliasFieldPlugin", (request, resolveContext, callback) => {
// 如果没有描述文件就执行回调,进入下一个函数
if (!request.descriptionFileData) return callback();
// 解析出请求文件的路径,比如'./src/index.js'
const innerRequest = c(resolver, request);
// 不存在请求的话执行回调进行下一个函数
if (!innerRequest) return callback();
// 从package.json中取出字段对应的内容,这里的field为browser
// browser 可定义成和 main/module 字段一一对应的映射对象,也可以直接定义为字符串
// "browser": {
// "./lib/index.js": "./lib/index.browser.js", // browser+cjs
// "./lib/index.mjs": "./lib/index.browser.mjs" // browser+mjs
// },
// 关于browser字段可以看这篇文章:[https://github.com/SunshowerC/blog/issues/8](https://github.com/SunshowerC/blog/issues/8)
const fieldData = DescriptionFileUtils.getField(
request.descriptionFileData,
this.field
);
// 如果browser字段的内容不是一个对象的话提示配置错误,必须为一个对象,不会终止解析,会执行回调进行下一个函数
if (typeof fieldData !== "object") {
if (resolveContext.log)
resolveContext.log(
"Field '" +
this.field +
"' doesn't contain a valid alias configuration"
);
return callback();
}
// './src/index.js' 对应的值
const data1 = fieldData[innerRequest];
// "src/index.js" 对应的值
const data2 = fieldData[innerRequest.replace(/^\.\//, "")];
// 相对路径和绝对路径兼容
const data = typeof data1 !== "undefined" ? data1 : data2;
// 如果值和innerRequest相同就执行回调进行下一个函数,因为innerRequest已经resolve了
if (data === innerRequest) return callback();
// 如果是undefined执行回调进行下一个函数
if (data === undefined) return callback();
// 如果是false阻止将模块或文件加载到包中
if (data === false) {
const ignoreObj = Object.assign({}, request, {
path: false,
});
return callback(null, ignoreObj);
}
// 将描述文件根路径和对应的文件路径合并到request
const obj = Object.assign({}, request, {
path: request.descriptionFileRoot,
request: data,
});
// 进行resolve
resolver.doResolve(
target,
obj,
"aliased from description file " +
request.descriptionFilePath +
" with mapping '" +
innerRequest +
"' to '" +
data +
"'",
resolveContext,
(err, result) => {
if (err) return callback(err);
// Don't allow other aliasing or raw request
if (result === undefined) return callback(null, null);
callback(null, result);
}
);
});
}
// node_modules/enhanced-resolve/lib/AliasFieldPlugin.js
/*
* 源配置
* alias: {
* 'vue$': 'vue/dist/vue.esm.js',
* '@': '../src'
* }
* 转换后为
* alias:[
* {
* name: 'vue',
* onlyModule: true,
* alias: 'vue/dist/vue.esm.js'
* },
* {
* name: '@',
* onlyModule: false,
* alias: '../src'
* }
* ]
*/
apply(resolver) {
const target = resolver.ensureHook(this.target);
// target 挂载为resolve
resolver
.getHook(this.source)
.tapAsync("AliasPlugin", (request, resolveContext, callback) => {
// 取出内部请求,比如 @/user/login.js
const innerRequest = request.request || request.path;
// 没有内部请求就执行回调,进行下一个alias解析
if (!innerRequest) return callback();
// 以这个为例 {name: '@', onlyModule: false, alias: '../src'}
for (const item of this.options) {
// request 是以 @/ 开头 或者就是 @(比如src/index.js 直接可以写成 @)
if (
innerRequest === item.name ||
(!item.onlyModule && startsWith(innerRequest, item.name + "/"))
) {
// 尚未被替换掉
if (
innerRequest !== item.alias &&
!startsWith(innerRequest, item.alias + "/")
) {
// 将name替换为alias @ ---> ../src
const newRequestStr =
item.alias + innerRequest.substr(item.name.length);
// 将新的请求合并至request
const obj = Object.assign({}, request, {
request: newRequestStr,
});
// 替换完就进行resolve
return resolver.doResolve(
target,
obj,
"aliased with mapping '" +
item.name +
"': '" +
item.alias +
"' to '" +
newRequestStr +
"'",
resolveContext,
(err, result) => {
if (err) return callback(err);
// Don't allow other aliasing or raw request
if (result === undefined) return callback(null, null);
callback(null, result);
}
);
}
}
}
return callback();
});
}
这一步就是处理别名的,它依赖于 package.json 中的配置,所以放在了 DescriptionFilePlugin 后面执行。AliasFieldPlugin 会先解析出 package.json中的 alias ,也就是 browser 字段设置的路径映射(CommonJS 和 ESModule)。除了我们自己配置的别名外,webpack 内部还有一些自带的 alias,每一个 alias 都会注册一个 AliasPlugin 函数进行处理,一旦匹配到 alias 的 name 就用新的别名 alias 替换 request 参数,然后进行 resolve,如果没有匹配到则进入 ModuleKindPlugin 函数。
ModuleKindPlugin 会根据 request.module(上面 ParsePlugin 返回的 module 值) 的值走不同的分支。如果是 module,则后续进入 rawModule 的逻辑,进行模块的 resolve。否则的话就返回 undefined,继续进入下一个处理函数 JoinRequestPlugin。
// node_modules/enhanced-resolve/lib/ModuleKindPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// 挂载 module hook
resolver
.getHook(this.source)
.tapAsync("ModuleKindPlugin", (request, resolveContext, callback) => {
// 如果不是module就执行下一个函数
if (!request.module) return callback();
// 如果是module就合并request,删除module字段然后以加载module的方式执行resolve
const obj = Object.assign({}, request);
delete obj.module;
resolver.doResolve(
target,
obj,
"resolve as module",
resolveContext,
(err, result) => {
if (err) return callback(err);
// Don't allow other alternatives
if (result === undefined) return callback(null, null);
callback(null, result);
}
);
});
}
JoinRequestPlugin 将 request 中 path 和 request 合并起来,将 request 中 relativePath 和 request 合并起来,得到两个完整的路径:
// node_modules/enhanced-resolve/lib/JoinRequestPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// 挂载 relative hook
resolver
.getHook(this.source)
.tapAsync("JoinRequestPlugin", (request, resolveContext, callback) => {
// 合并路径得到 path: '/Users/***/lagou-edu/webpack-entry/src/index.js' relativePath: undefind
const obj = Object.assign({}, 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);
});
}
JoinRequestPlugin 合并完毕后将合并结果合并至 request,然后进行 resolve,执行挂载的钩子 relative,这个钩子上同样注册了两个钩子函数:DescriptionFilePlugin 和 NextPlugin,不过与上面 DescriptionFilePlugin不同的是 ,此时的 request.path 变成了 /Users/***/lagou-edu/webpack-entry/src/index.js,由于 path 改变了,所以需要再次查找一下 package.json,进行路径映射匹配,最终进行 resolve,此时的 hook 已经挂载为 describedRelative。
执行完 JoinRequestPlugin 的 stack 里面就有 5 个内容了:
0:"resolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
5)第五个钩子就是上面第二次执行 DescriptionFilePlugin 时挂载的钩子 describedRelative,它上面注册有钩子函数 FileKindPlugin 和 TryNextPlugin:
FileKindPlugin 函数判断目标是否为一个 directory,如果是则返回 undefined, 进入下一个 tryNextPlugin 函数,这时会进入加载 directory 的分支。否则,则表明是一个文件,进入 rawFile 事件:
// node_modules/enhanced-resolve/lib/FileKindPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// 挂载 rawFile hook
resolver
.getHook(this.source)
.tapAsync("FileKindPlugin", (request, resolveContext, callback) => {
// 如果是一个文件夹,执行回调,返回undefined,会进入下一个函数tryNextPlugin
if (request.directory) return callback();
// 否则的话就是一个file,不可能是module了,因为前面已经判断过module的逻辑
// 合并request,进行resolve
const obj = Object.assign({}, request);
// 因为不是文件夹,删除它的directory
delete obj.directory;
resolver.doResolve(target, obj, null, resolveContext, callback);
});
}
tryNextPlugin 函数起一个衔接的作用,直接进行 resolve,进入下一个流程:
// node_modules/enhanced-resolve/lib/TryNextPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// 挂载 directory hook(上面判断是否为文件夹,如果是,这里的hook就挂载为directory,然后进行resolve,执行directory上注册的函数)、
// node_modules/enhanced-resolve/lib/ResolverFactory.js 275行
// plugins.push(
// new TryNextPlugin("described-relative", "as directory", "directory")
// );
resolver
.getHook(this.source)
.tapAsync("TryNextPlugin", (request, resolveContext, callback) => {
resolver.doResolve(
target,
request,
this.message,
resolveContext,
callback
);
});
}
describedRelative 执行完 stack 就有 6 个内容了:
0:"resolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js)
6)第六个就是 FileKindPlugin 挂载的钩子 rawFile,它上面注册了 5 个函数:
TryNextPlugin --> AppendPlugin --> AppendPlugin --> AppendPlugin --> AppendPlugin
这里的 TryNextPlugin 主要是挂载 file hook:
// node_modules/enhanced-resolve/lib/TryNextPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// 挂载 file hook
resolver
.getHook(this.source)
.tapAsync("TryNextPlugin", (request, resolveContext, callback) => {
// 过渡到file hook 执行流程
resolver.doResolve(
target,
request,
this.message,
resolveContext,
callback
);
});
}
webpack 的 resolve.enforceExtension 决定加载文件时是否可以省略扩展名,默认为 false,也就是默认可以省略的,如果设置为 true 的话,我们加载文件就必须带上文件扩展名,否则就会找不到文件;resolve.extensions 尝试按顺序解析这些后缀名,如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首个匹配到的后缀的文件并跳过其余的后缀,比如 ['.js', '.json', '.scss'],当存在相同命名但后缀不同的文件 ”user.js“、”user.json“、”user.scss“ 时,如果我们导入文件时省略了后缀 import/src/user ,那么 webpack 在解析到”user.js“后就会跳过数组后面的几个后缀。
AppendPlugin 主要是添加上文件扩展名,将 extensions 中的匹配的路径拼接到 request.path 和 request.relativePath 上:
// node_modules/enhanced-resolve/lib/AppendPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// let extensions = options.extensions || [".js", ".json", ".node"];
// extensions.forEach(item => {
// plugins.push(new AppendPlugin("raw-file", item, "file"));
// });
// 这里根据extensions生成AppendPlugin插件
// 挂载file hook,后缀添加完回到 file 流程
resolver
.getHook(this.source)
.tapAsync("AppendPlugin", (request, resolveContext, callback) => {
const obj = Object.assign({}, request, {
path: request.path + this.appending,
relativePath:
request.relativePath && request.relativePath + this.appending
});
resolver.doResolve(
target,
obj,
this.appending,
resolveContext,
callback
);
});
}
此时 stack 就有 7 个内容了:
0:"resolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
6:"rawFile: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
7)第七个就是 AppendPlugin 挂载的钩子 file,它上面注册了 3 个钩子函数:
AliasFieldPlugin --> SymlinkPlugin --> FileExistsPlugin
此时路径后缀已经添加上了,又回到了 AliasFieldPlugin 处理 alias 的逻辑,这里只会处理 package.json 中的 browser 配置的路径映射,webpack 自带的 alias 不再重复处理,如果匹配到就进行 resolve(AliasFieldPlugin 里的 target 挂载为 resolve),否则就执行回调,返回 undefined,进入 SymlinkPlugin。
SymlinkPlugin 用来处理路径中存在 link 的情况,webpack 默认是按照真实路径进行解析的,如果解析途中遇到 link 会替换为真实路径:
// node_modules/enhanced-resolve/lib/SymlinkPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// 挂载 relative hook,替换完进入 relative 流程,因为 path 改变了,需要重新加载 package.json 查找路径映射
const fs = resolver.fileSystem;
resolver
.getHook(this.source)
.tapAsync("SymlinkPlugin", (request, resolveContext, callback) => {
// 分解path,得到两个数组 paths 和 seqments
const pathsResult = getPaths(request.path);
// ['index.js', 'src', 'webpack-entry', 'lagou-edu', 'xxx', 'Users', '/']
const pathSeqments = pathsResult.seqments;
// 0:'/Users/---/lagou-edu/webpack-entry/src/index.js'
// 1:'/Users/---/lagou-edu/webpack-entry/src'
// 2:'/Users/---/lagou-edu/webpack-entry'
// 3:'/Users/---/lagou-edu'
// 4:'/Users/---'
// 5:'/Users'
// 6:'/'
const paths = pathsResult.paths;
// 包含link标识,默认为false
let containsSymlink = false;
forEachBail.withIndex(
paths,
(path, idx, callback) => {
// 读取link
fs.readlink(path, (err, result) => {
if (!err && result) {
// 将pathSeqments里面的值替换为读取到的结果
pathSeqments[idx] = result;
// 设置标识为true
containsSymlink = true;
// 找到绝对符号链接的快捷路径,就进入下一个函数处理link(symlink必须是一个绝对路径,不可能是相对路径)
// 这个正则就是检测结果是不是一个绝对路径,是的话才进入下面的函数处理link
if (/^(\/|[a-zA-Z]:($|\\))/.test(result))
return callback(null, idx);
}
// 读取不到就执行回调,返回undefined,执行下一个函数FileExistsPlugin
callback();
});
},
(err, idx) => {
// 如果不存在link执行下一个函数FileExistsPlugin
if (!containsSymlink) return callback();
// 将link替换为fs.readlink读取到的真实路径
const resultSeqments =
typeof idx === "number"
? pathSeqments.slice(0, idx + 1)
: pathSeqments.slice();
const result = resultSeqments.reverse().reduce((a, b) => {
return resolver.join(a, b);
});
// 将替换后的路径合并至request
const obj = Object.assign({}, request, {
path: result,
});
// 进入下一个流程 relative
resolver.doResolve(
target,
obj,
"resolved symlink to " + result,
resolveContext,
callback
);
}
);
});
}
resolve.symlink 是否将符号链接(symlink)解析到它们的符号链接位置(symlink location)。启用时,符号链接(symlink)的资源,将解析为其 真实 路径,而不是其符号链接(symlink)的位置。注意,当使用创建符号链接包的工具(如 npm link)时,这种方式可能会导致模块解析失败。所以这种方式只能在开发期间使用,正式环境下需要将独立开发的包发布到 npm,然后安装到使用项目中。
关于 symlink,在使用 lerna 管理多个包的开发时,在某个包下使用 npm link 创建这个包的软链,在使用这个包的 package 下通过 npm link packageName 来创建一个链接,此时在 node_modules 下就会存在这个名称为 packageName 的包,不过它不是真实存在于这个使用方的依赖下,而是一个 symlink,当使用 webpack 解析到这个请求时,就是尝试使用 SymlinkPlugin 将 request path 替换为真实的路径,然后重新回到 relative 流程,查找是否存在路径映射。
如果不存在 symlink,或者解析异常,路径不是绝对路径都会执行 callback,返回 undefined,进入下一个函数 FileExistsPlugin。
FileExistsPlugin 检测文件是否存在,存在的话就会进入下一个流程 existing-file :
// node_modules/enhanced-resolve/lib/FileExistsPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// 挂载 existing-file hook
const fs = resolver.fileSystem;
resolver
.getHook(this.source)
.tapAsync("FileExistsPlugin", (request, resolveContext, callback) => {
const file = request.path;
// 检测路径
fs.stat(file, (err, stat) => {
// 如果检测出错或者没有检测结果,记录错误并打印日志(配置了的话),执行回调
if (err || !stat) {
if (resolveContext.missing) resolveContext.missing.add(file);
if (resolveContext.log) resolveContext.log(file + " doesn't exist");
return callback();
}
// 如果检测的结果不是一个文件,记录错误并打印日志(配置了的话),执行回调
if (!stat.isFile()) {
if (resolveContext.missing) resolveContext.missing.add(file);
if (resolveContext.log) resolveContext.log(file + " is not a file");
return callback();
}
// 否则进行下一个流程existing-file
resolver.doResolve(
target,
request,
"existing file: " + file,
resolveContext,
callback
);
});
});
}
此时 stack 就有 8 个内容了:
0:"resolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
6:"rawFile: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
7:"file: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
8)第八个就是 FileExistsPlugin 挂载的钩子 existing-file,它上面注册了一个函数 NextPlugin,直接进入下一个流程:
// node_modules/enhanced-resolve/lib/NextPlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
// 挂载 resolved hook
resolver
.getHook(this.source)
.tapAsync("NextPlugin", (request, resolveContext, callback) => {
resolver.doResolve(target, request, null, resolveContext, callback);
});
}
此时 stack 就有 9 个内容了:
0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
6:"rawFile: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
7:"file: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
8:"existingFile: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
9)第九个就是 FileExistsPlugin 挂载的钩子 resolved,它上面注册了一个函数 ResultPlugin:
// node_modules/enhanced-resolve/lib/ResultPlugin.js
apply(resolver) {
this.source.tapAsync("ResultPlugin", (request, resolverContext, callback) => {
// 合并request
const obj = Object.assign({}, request);
// 报告结果路径
if (resolverContext.log)
resolverContext.log("reporting result " + obj.path);
// 执行resolver钩子上挂载的result钩子,如果有注册函数就执行没有就直接执行回调
// 这里的回调时doResolve hook执行的最终回调
// return hook.callAsync(request, innerContext, (err, result) => {
// if (err) return callback(err);
// if (result) return callback(null, result);
// callback();
// });
resolver.hooks.result.callAsync(obj, resolverContext, (err) => {
if (err) return callback(err);
callback(null, obj);
});
});
}
ResultPlugin 最终先执行 result 钩子上注册的函数,没有注册或者执行完会最终执行函数 doResolve 中 hook 执行的最终回调,然后走到 resolve 中调用 doResolve 的回调
此时 stack 就有 10 个内容了:
0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
6:"rawFile: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
7:"file: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
8:"existingFile: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
9:"resolved: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
最后我们看看 ResolverFactory 中挂载的钩子:
// node_modules/enhanced-resolve/lib/ResolverFactory.js
//// pipeline ////
resolver.ensureHook("resolve");
resolver.ensureHook("parsedResolve");
resolver.ensureHook("describedResolve");
resolver.ensureHook("rawModule");
resolver.ensureHook("module");
resolver.ensureHook("relative");
resolver.ensureHook("describedRelative");
resolver.ensureHook("directory");
resolver.ensureHook("existingDirectory");
resolver.ensureHook("undescribedRawFile");
resolver.ensureHook("rawFile");
resolver.ensureHook("file");
resolver.ensureHook("existingFile");
resolver.ensureHook("resolved");
除了 module、directory、existingDirectory、undescribedRawFile 几个钩子外,其它的 10 个钩子全部在我们最终的 stack 中,这就是我们加载一个普通 file 的整个流程,也就是所谓的 pipeline(管道思想)。
module的doResolve
在 ParsePlugin 的时候就会标识加载的目标是不是一个module,模块其实就类似于import vue from 'vue'这样的第三方模块加载,它也是执行挂载在hook上的钩子函数,一步步处理,最终会转化为directory来处理。而load最终也会转化为一个普通module来处理。