0. 食用本文的文档阐明:
因为篇幅无限,心愿你把握以下前置条件:
- 心愿你最好理解 订阅公布模型
- 心愿你晓得
tapable
的 以下 3 个钩子函数AsyncSeriesBailHook, AsyncSeriesHook, SyncHook
通过本文你将学到如下内容(或者带着如下疑难去学习)
- 如何
调试
一个 nodejs 开源库 - 理解
webpack
解析库 enhance-resolve 的大抵工作流程 - 初步理解 webpack/enhance-resolve 中
tapable
的应用,以及插件机制实现的原理
(这里写 webpack,是因为二者的插件机制是一样的实现原理)
本文 GitHub 解析地址:
fu1996/enhanced-resolve at feature-study-enhanced (github.com),先看全文,再思考要不要给个star
⭐️。
1. 初步理解该库的作用,明确这个库是干啥的?
想初步理解一个库的作用,以及建设初衷,最好的形式就是浏览以后库的README.md
(前提是该库作者保护了此文档 )。
README.md
内容如下:
翻译为中文就是:
大家想理解对于原生的 require.resolve 的介绍能够看这篇文章 ===> node 的门路解析 require.resolve - 掘金 (juejin.cn)
该库也是作为 webpack
里外围的依赖解析库存在,在 webpack.config.js
里配置的 resolve
字段 实际上就是当做参数传递给该库的,所以深刻的理解一下该库的工作原理以及插件机制的实现,也有益于 webpack
的优化 和 前期浏览 webpack
源码。
2. 拉取并跑起来一个简略的 demo,初步理解该库对于 resolve 的 enhance (加强)
GitHub 地址如下:webpack/enhanced-resolve: Offers an async require.resolve function. It's highly configurable. (github.com) PS: 国外拜访较慢,强烈推荐 应用 Gitee
导入该仓库 【不会吧,不会吧,都 2023 年了,居然还有人不晓得这个办法?】
代码拉取结束当前,察看我的项目目录,发现应用的 yarn,执行 yarn install
进行装置依赖装置。如果没报错的话,写一个简略的 demo 小试牛刀。
新建一个 demo
文件夹,并创立 test-hook.js
(名称能够自定义),而后写入如下内容:
const { ResolverFactory, CachedInputFileSystem } = require("../lib");const fs = require("fs");const path = require("path");const myResolver = ResolverFactory.createResolver({ fileSystem: new CachedInputFileSystem(fs, 4000), extensions: [".json", ".js", ".ts"],});const context = {};const resolveContext = {};const lookupStartPath = path.resolve(__dirname);const request = "./a";myResolver.resolve( context, lookupStartPath, request, resolveContext, (err, path, result) => { if (err) { console.log("createResolve err: ", err); } else { console.log("createResolve path: ", path); } });
新建 a.js
文件(不用写入内容,该库只做门路解析
), 此时文件目录如下:
运行test-hook.js
输入如下:
demo 运行胜利
,第 2 关通过
3. 开启 Debug 模式,剖析大体逻辑
自己喜爱用 webStorm 进行调试 (之前是搞 Python 开发的,习惯了)。
3.1 webStorm 应用 debug 模式 (不是本文重点,简略阐明一下)
webStorm 的只须要 以后文件 下 右击
,而后 点击 Debug test-hook.js
即可
’
3.2 vscode 应用 debug 模式
vscode 的 debug 形式很多,这里只说一个 自带 debug 终端 的调试办法,此法也是很不便调试 node 程序的。
点击结束当前,产生一个新的终端:(下面的 ws 地址 请自行摸索)
新的终端默认是在 根目录下的,轻易在 test-hook.js 打一个 断点,而后 运行 node 命令:
node demo/test-hook.js
它就进来了。
3.2 剖析大体逻辑
3.2.1 应用 ResolverFactory
工厂类 调用 createResolver
办法 创立一个 resolver 实例
const myResolver = ResolverFactory.createResolver({ fileSystem: new CachedInputFileSystem(fs, 4000), extensions: [".json", ".js", ".ts"],});
我看到这段的代码的次要逻辑就是去想:这办法吃了啥?吐出了啥?能依据变量名失去啥?
而后再去看办法的大抵实现。
- 这办法吃了 相似于 webpack resolver 里的配置
- 从命名来猜想 这办法吐出了 一个 myResolver 的 对象
3.2.2 进入 createResolver
办法 大抵剖析流程 (进入该办法:按住 Ctrl + 【鼠标左键点击】)
这里只贴局部外围代码
exports.createResolver = function (options) { // 解析并规范化用户传入的配置 const normalizedOptions = createOptions(options); const { plugins: userPlugins, } = normalizedOptions; // 深拷贝一下 用户用到的 plugins const plugins = userPlugins.slice(); // 依据配置创立 resolver 实例 const resolver = customResolver ? customResolver : new Resolver(fileSystem, normalizedOptions); //// pipeline //// // 确保该 hook 存在,不存在则注册它 resolver.ensureHook("resolve"); resolver.ensureHook("internalResolve"); // 依据配置 把用到的 内置 plugin 丢到 plugins 列表里 // 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")); } } // ...省略局部plugins.push的逻辑代码... //// RESOLVER //// // 遍历 plugins 列表 并传入resolver 实例 for (const plugin of plugins) { if (typeof plugin === "function") { // 是函数 this 指向 resolver plugin.call(resolver, resolver); } else { // 是类, 开始调用apply 办法 ,apply 办法 会注册一些 下面ensure的 hook plugin.apply(resolver); } } // 返回resolve 对象 return resolver;};
一个简略的流程图如下:
plugin.apply(resolver);
所有的事件 都曾经胜利订阅。
所有的钩子都在 resolver
对象 身上了 (子弹曾经上膛,筹备发射)。
3.3 粗略过下 Resolver
类的办法
咱们应用 resolver 的 形式如下:
const context = {};const resolveContext = {};const lookupStartPath = path.resolve(__dirname);const request = "./a";myResolver.resolve( context, lookupStartPath, request, resolveContext, (err, path, result) => { if (err) { console.log("createResolve err: ", err); } else { console.log("createResolve path: ", path); } });
那第一步就是 看 resolve
办法
3.3.1 初步理解 resolve
办法
外围代码如下:
看源代码时候不能心急,第一步 应该保大丢小,先把握全局视角,而后一一深刻,看到前期,会有豁然开朗的感觉,原来那块写的是这个意思啊。
class Resolver { resolve(context, path, request, resolveContext, callback) { // 所有流程的外围 就是这个 obj 对象 const obj = { context: context, path: path, request: request, }; const message = `resolve '${request}' in '${path}'`; const finishResolved = (result) => { return callback( null, result.path === false ? false : `${result.path.replace(/#/g, "\0#")}${ result.query ? result.query.replace(/#/g, "\0#") : "" }${result.fragment || ""}`, result ); }; const finishWithoutResolve = (log) => { /` * @type {Error & {details?: string}} */ const error = new Error("Can't " + message); error.details = log.join("\n"); this.hooks.noResolve.call(obj, error); return callback(error); }; if (resolveContext.log) { // We need log anyway to capture it in case of an error const parentLog = resolveContext.log; const log = []; return this.doResolve( this.hooks.resolve, obj, message, { log: (msg) => { parentLog(msg); log.push(msg); }, yield: yield_, fileDependencies: resolveContext.fileDependencies, contextDependencies: resolveContext.contextDependencies, missingDependencies: resolveContext.missingDependencies, stack: resolveContext.stack, }, (err, result) => { if (err) return callback(err); if (yieldCalled || (result && yield_)) return finishYield(result); if (result) return finishResolved(result); return finishWithoutResolve(log); } ); } else { // Try to resolve assuming there is no error // We don't log stuff in this case return this.doResolve( this.hooks.resolve, obj, message, { log: undefined, yield: yield_, fileDependencies: resolveContext.fileDependencies, contextDependencies: resolveContext.contextDependencies, missingDependencies: resolveContext.missingDependencies, stack: resolveContext.stack, }, (err, result) => { if (err) return callback(err); if (yieldCalled || (result && yield_)) return finishYield(result); if (result) return finishResolved(result); // log is missing for the error details // so we redo the resolving for the log info // this is more expensive to the success case // is assumed by default const log = []; return this.doResolve( this.hooks.resolve, obj, message, { log: (msg) => log.push(msg), yield: yield_, stack: resolveContext.stack, }, (err, result) => { if (err) return callback(err); // In a case that there is a race condition and yield will be called if (yieldCalled || (result && yield_)) return finishYield(result); return finishWithoutResolve(log); } ); } ); } }}
大抵看完,发现这一步其实也是依据不同的条件去组装数据,把传入的数据,赋值到 obj 对象上,而后把 obj 对象传入doResolve
办法,当做此办法的第二个参数,真正调用的还是 doResolve
办法,下一步就是大抵瞅下doResolve
办法。
3.3.2 初步理解 doResolve
办法
下面resolve
传递的 obj 对象作为 doResolve 的第二个参数,命名为:request
,一起来看下。
doResolve(hook, request, message, resolveContext, callback) { // 静态方法 依据以后 hook 信息 生成 调用栈信息 const stackEntry = Resolver.createStackEntry(hook, request); let newStack; // 以后 hook 调用栈信息 存入 newStack 里 if (resolveContext.stack) { newStack = new Set(resolveContext.stack); if (resolveContext.stack.has(stackEntry)) { /` * Prevent recursion * @type {Error & {recursion?: boolean}} */ const recursionError = new Error( "Recursion in resolving\nStack:\n " + Array.from(newStack).join("\n ") ); recursionError.recursion = true; if (resolveContext.log) resolveContext.log("abort resolving because of recursion"); return callback(recursionError); } newStack.add(stackEntry); } else { newStack = new Set([stackEntry]); } // 传入 hook, request 调用 resolveStep 的 hook this.hooks.resolveStep.call(hook, request); // 如果以后hook 被应用了 if (hook.isUsed()) { const innerContext = createInnerContext( { log: resolveContext.log, yield: resolveContext.yield, fileDependencies: resolveContext.fileDependencies, contextDependencies: resolveContext.contextDependencies, missingDependencies: resolveContext.missingDependencies, stack: newStack }, message ); // 触发以后hook 并传入 request 和 innerContext 当做参数 return hook.callAsync(request, innerContext, (err, result) => { if (err) return callback(err); if (result) return callback(null, result); callback(); }); } else { // 执行 callback 逻辑 callback(); }}
callback
的逻辑比较简单,咱们应该看以后 hook
(指的是:this.hooks.resolve)被应用的时候,resolve 的解决逻辑。
要害代码如下:
hook.callAsync(request, innerContext, (err, result) => {
以后 hook 间接调用了 callAsync
进行了 触发之前 plugin
的订阅事件,这时候咱们要去找到之前 plugin.apply(resolver);
的时候,哪一个 plugin
的订阅类型 为resolve
事件。
3.3.3 去 ResolverFactory.js
文件寻找注册了 resolve 事件的 钩子
场景切回到 ResolverFactory.js
文件,不言而喻的在 327
行左右 看到了这个注册事件,此 demo 的 unsafeCache
为false
所以此处 执行的是 347
行的代码 (对于此参数的作用,先 TODO 下
,第一次看源码不能追深,应该追广)。这次要进入ParsePlugin
插件里,看它到底实现了哪些逻辑。(优良的开源库,对于事件和数据的解决就是这么 callback
,必须急躁 )
3.3.4 去 ParsePlugin
插件里,看最初一层的解决逻辑,实现闭环
ParsePlugin
插件,是以后主流程 闭环的完结,也是 文件解析
流程的开始,因为 从文章结尾开始到当初,还没有真正的针对 文件解析
相干的事件 做相干操作,全是在注册一些 hook
,实例化 Resolve
对象,解决格式化入参。
上代码,看具体逻辑,现身吧 我的小宝贝
:
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra*/"use strict";/` @typedef {import("./Resolver")} Resolver *//` @typedef {import("./Resolver").ResolveRequest} ResolveRequest *//` @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */module.exports = class ParsePlugin { /` * @param {string | ResolveStepHook} source source * @param {Partial<ResolveRequest>} requestOptions request options * @param {string | ResolveStepHook} target target */ constructor(source, requestOptions, target) { // 承受参数 并绑定到this 上 this.source = source; this.requestOptions = requestOptions; this.target = target; } /` * @param {Resolver} resolver the resolver * @returns {void} */ apply(resolver) { // 这个resolver 就是之前 创立的 Resolver 的实体类 const target = resolver.ensureHook(this.target); resolver // 失去 this.source 对应的 hook .getHook(this.source) // 监听 this.source 对应的 hook,并设置 订阅函数 .tapAsync("ParsePlugin", (request, resolveContext, callback) => { // 先初步解析 失去大抵后果: const parsed = resolver.parse(/` @type {string} */ (request.request)); // 合并参数 const obj = { ...request, ...parsed, ...this.requestOptions }; if (request.query && !parsed.query) { obj.query = request.query; } if (request.fragment && !parsed.fragment) { obj.fragment = request.fragment; } if (parsed && resolveContext.log) { if (parsed.module) resolveContext.log("Parsed request is a module"); if (parsed.directory) resolveContext.log("Parsed request is a directory"); } // There is an edge-case where a request with # can be a path or a fragment -> try both if (obj.request && !obj.query && obj.fragment) { const directory = obj.fragment.endsWith("/"); const alternative = { ...obj, directory, request: obj.request + (obj.directory ? "/" : "") + (directory ? obj.fragment.slice(0, -1) : obj.fragment), fragment: "" }; // 这个 hook 做完了 它该做的事件了 进入 this.target 的 hook 逻辑吧, // 并把以后hook 解决过的后果传递给this.target 的 hook resolver.doResolve( target, alternative, null, resolveContext, (err, result) => { if (err) return callback(err); if (result) return callback(null, result); resolver.doResolve(target, obj, null, resolveContext, callback); } ); return; } resolver.doResolve(target, obj, null, resolveContext, callback); }); }};
你会发现这个插件 的确开始 进行 request
字段的解析了,终于 它开始剖析你在 test-hook.js
传入的 "./a" 到底是文件夹,还是文件了。
const request = "./a";
在该插件又通过一系列的解析当前,发现又开始应用 resolver.doResolve
办法 流转到 this.target
的 hook 了。
场景回溯:
先回溯一下以后的 this.target 是代表的那个参数?
plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));
而后回忆一下resolver.doResolve
办法做了啥? 此时 hook 的入参是 "parsed-resolve"
, request
参数代表的是 resolve
hook 解决过的 alternative
变量。
doResolve(hook, request, message, resolveContext, callback) { // 静态方法 依据以后 hook 信息 生成 调用栈信息 const stackEntry = Resolver.createStackEntry(hook, request); let newStack; // 以后 hook 调用栈信息 存入 newStack 里 if (resolveContext.stack) { newStack = new Set(resolveContext.stack); if (resolveContext.stack.has(stackEntry)) { /` * Prevent recursion * @type {Error & {recursion?: boolean}} */ const recursionError = new Error( "Recursion in resolving\nStack:\n " + Array.from(newStack).join("\n ") ); recursionError.recursion = true; if (resolveContext.log) resolveContext.log("abort resolving because of recursion"); return callback(recursionError); } newStack.add(stackEntry); } else { newStack = new Set([stackEntry]); } // 传入 hook, request 调用 resolveStep 的 hook this.hooks.resolveStep.call(hook, request); // 如果以后hook 被应用了 if (hook.isUsed()) { const innerContext = createInnerContext( { log: resolveContext.log, yield: resolveContext.yield, fileDependencies: resolveContext.fileDependencies, contextDependencies: resolveContext.contextDependencies, missingDependencies: resolveContext.missingDependencies, stack: newStack }, message ); // 触发以后hook 并传入 request 和 innerContext 当做参数 return hook.callAsync(request, innerContext, (err, result) => { if (err) return callback(err); if (result) return callback(null, result); callback(); }); } else { // 执行 callback 逻辑 callback(); }}
所以以后的this.target
指的是parsed-resolve
相干的 hook,相当的见名知意。至于接下来的流程,打算另开一篇文章去 讲解 resolver
具体的 hook
流转过程,感兴趣的兄弟们能够本人拉代码进行学习。
4. 完结撒花
终于,通过了一路的兜兜转转,这个 resolve 终于开始解析了。来张流程图,总结一下全文。
- ResolverFactory.createResolver 依据
Resolver
类创立实例:myResolve
(吃了配置,吐出对象myResolve
) myResolve 上 注册并订阅
大量的 hook (枪支弹药贮备好,一刻激发)- 调用
myResolver.resolve
办法开始进行 文件解析 的主流程 - 外部通过
resolve.doResolve
办法,开始调用第一个 hook:this.hooks.resolve
- 找到之前 订阅 hook 的 plugin:
ParsePlugin
ParsePlugin
进行初步解析,而后 通过doResolve
执行下一个 hookparsed-resolve
,后期筹备工作完结,链式调用开始,真正的解析文件的流程
也开始。
本文 GitHub 解析地址:
fu1996/enhanced-resolve at feature-study-enhanced (github.com),看到这里,如果感觉头痒(是要长常识了),学到了一丢丢常识,欢送各位大佬点start
。
初步确定下一篇文档:enhance-resolve 中的数据流动。