webpack源码分析(九)
回归上节内容,我们讲了一个ResolverFactory的demo,得知,他有一个resolve方法,可以让我们拿到具体的路径。
接下来,我们分析代码,他是怎么运行的。
exports.createResolver = function (options) { // 解析并规范化用户传入的配置 const normalizedOptions = createOptions(options); const { alias, fallback, aliasFields, cachePredicate, cacheWithContext, conditionNames, descriptionFiles, enforceExtension, exportsFields, extensionAlias, importsFields, extensions, fileSystem, fullySpecified, mainFields, mainFiles, modules, plugins: userPlugins, pnpApi, resolveToContext, preferRelative, preferAbsolute, symlinks, unsafeCache, resolver: customResolver, restrictions, roots } = normalizedOptions; const plugins = userPlugins.slice(); const resolver = customResolver ? customResolver : new Resolver(fileSystem, normalizedOptions); //// pipeline //// // 确保hook 存在 resolver.ensureHook("resolve"); resolver.ensureHook("internalResolve"); resolver.ensureHook("newInternalResolve"); resolver.ensureHook("parsedResolve"); resolver.ensureHook("describedResolve"); resolver.ensureHook("rawResolve"); resolver.ensureHook("normalResolve"); resolver.ensureHook("internal"); resolver.ensureHook("rawModule"); resolver.ensureHook("module"); resolver.ensureHook("resolveAsModule"); resolver.ensureHook("undescribedResolveInPackage"); resolver.ensureHook("resolveInPackage"); resolver.ensureHook("resolveInExistingDirectory"); resolver.ensureHook("relative"); resolver.ensureHook("describedRelative"); resolver.ensureHook("directory"); resolver.ensureHook("undescribedExistingDirectory"); resolver.ensureHook("existingDirectory"); resolver.ensureHook("undescribedRawFile"); resolver.ensureHook("rawFile"); resolver.ensureHook("file"); resolver.ensureHook("finalFile"); resolver.ensureHook("existingFile"); resolver.ensureHook("resolved"); // TODO remove in next major // cspell:word Interal // Backward-compat // @ts-ignore resolver.hooks.newInteralResolve = resolver.hooks.newInternalResolve; // resolve for (const { source, resolveOptions } of [ { source: "resolve", resolveOptions: { fullySpecified } }, { source: "internal-resolve", resolveOptions: { fullySpecified: false } } ]) { if (unsafeCache) { plugins.push( new UnsafeCachePlugin( source, cachePredicate, /** @type {import("./UnsafeCachePlugin").Cache} */ (unsafeCache), cacheWithContext, `new-${source}` ) ); plugins.push( new ParsePlugin(`new-${source}`, resolveOptions, "parsed-resolve") ); } else { plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve")); } } // parsed-resolve plugins.push( new DescriptionFilePlugin( "parsed-resolve", descriptionFiles, false, "described-resolve" ) ); plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve")); // described-resolve plugins.push(new NextPlugin("described-resolve", "raw-resolve")); if (fallback.length > 0) { plugins.push( new AliasPlugin("described-resolve", fallback, "internal-resolve") ); } // raw-resolve if (alias.length > 0) { plugins.push(new AliasPlugin("raw-resolve", alias, "internal-resolve")); } aliasFields.forEach(item => { plugins.push(new AliasFieldPlugin("raw-resolve", item, "internal-resolve")); }); extensionAlias.forEach(item => plugins.push( new ExtensionAliasPlugin("raw-resolve", item, "normal-resolve") ) ); plugins.push(new NextPlugin("raw-resolve", "normal-resolve")); // normal-resolve if (preferRelative) { plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative")); } 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" ) ); if (preferAbsolute) { plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative")); } if (roots.size > 0) { plugins.push(new RootsPlugin("after-normal-resolve", roots, "relative")); } if (!preferRelative && !preferAbsolute) { plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative")); } // internal importsFields.forEach(importsField => { plugins.push( new ImportsFieldPlugin( "internal", conditionNames, importsField, "relative", "internal-resolve" ) ); }); // raw-module exportsFields.forEach(exportsField => { plugins.push( new SelfReferencePlugin("raw-module", exportsField, "resolve-as-module") ); }); modules.forEach(item => { if (Array.isArray(item)) { if (item.includes("node_modules") && pnpApi) { plugins.push( new ModulesInHierarchicalDirectoriesPlugin( "raw-module", item.filter(i => i !== "node_modules"), "module" ) ); plugins.push( new PnpPlugin("raw-module", pnpApi, "undescribed-resolve-in-package") ); } else { plugins.push( new ModulesInHierarchicalDirectoriesPlugin( "raw-module", item, "module" ) ); } } else { plugins.push(new ModulesInRootPlugin("raw-module", item, "module")); } }); // module plugins.push(new JoinRequestPartPlugin("module", "resolve-as-module")); // resolve-as-module if (!resolveToContext) { plugins.push( new ConditionalPlugin( "resolve-as-module", { directory: false, request: "." }, "single file module", true, "undescribed-raw-file" ) ); } plugins.push( new DirectoryExistsPlugin( "resolve-as-module", "undescribed-resolve-in-package" ) ); // undescribed-resolve-in-package plugins.push( new DescriptionFilePlugin( "undescribed-resolve-in-package", descriptionFiles, false, "resolve-in-package" ) ); plugins.push( new NextPlugin("after-undescribed-resolve-in-package", "resolve-in-package") ); // resolve-in-package exportsFields.forEach(exportsField => { plugins.push( new ExportsFieldPlugin( "resolve-in-package", conditionNames, exportsField, "relative" ) ); }); plugins.push( new NextPlugin("resolve-in-package", "resolve-in-existing-directory") ); // resolve-in-existing-directory plugins.push( new JoinRequestPlugin("resolve-in-existing-directory", "relative") ); // relative plugins.push( new DescriptionFilePlugin( "relative", descriptionFiles, true, "described-relative" ) ); plugins.push(new NextPlugin("after-relative", "described-relative")); // described-relative if (resolveToContext) { plugins.push(new NextPlugin("described-relative", "directory")); } else { plugins.push( new ConditionalPlugin( "described-relative", { directory: false }, null, true, "raw-file" ) ); plugins.push( new ConditionalPlugin( "described-relative", { fullySpecified: false }, "as directory", true, "directory" ) ); } // directory plugins.push( new DirectoryExistsPlugin("directory", "undescribed-existing-directory") ); if (resolveToContext) { // undescribed-existing-directory plugins.push(new NextPlugin("undescribed-existing-directory", "resolved")); } else { // undescribed-existing-directory plugins.push( new DescriptionFilePlugin( "undescribed-existing-directory", descriptionFiles, false, "existing-directory" ) ); mainFiles.forEach(item => { plugins.push( new UseFilePlugin( "undescribed-existing-directory", item, "undescribed-raw-file" ) ); }); // described-existing-directory mainFields.forEach(item => { plugins.push( new MainFieldPlugin( "existing-directory", item, "resolve-in-existing-directory" ) ); }); mainFiles.forEach(item => { plugins.push( new UseFilePlugin("existing-directory", item, "undescribed-raw-file") ); }); // undescribed-raw-file plugins.push( new DescriptionFilePlugin( "undescribed-raw-file", descriptionFiles, true, "raw-file" ) ); plugins.push(new NextPlugin("after-undescribed-raw-file", "raw-file")); // raw-file plugins.push( new ConditionalPlugin( "raw-file", { fullySpecified: true }, null, false, "file" ) ); if (!enforceExtension) { plugins.push(new TryNextPlugin("raw-file", "no extension", "file")); } extensions.forEach(item => { plugins.push(new AppendPlugin("raw-file", item, "file")); }); // file if (alias.length > 0) plugins.push(new AliasPlugin("file", alias, "internal-resolve")); aliasFields.forEach(item => { plugins.push(new AliasFieldPlugin("file", item, "internal-resolve")); }); plugins.push(new NextPlugin("file", "final-file")); // final-file plugins.push(new FileExistsPlugin("final-file", "existing-file")); // existing-file if (symlinks) plugins.push(new SymlinkPlugin("existing-file", "existing-file")); plugins.push(new NextPlugin("existing-file", "resolved")); } const resolved = /** @type {KnownHooks & EnsuredHooks} */ (resolver.hooks).resolved; // resolved if (restrictions.size > 0) { plugins.push(new RestrictionsPlugin(resolved, restrictions)); } plugins.push(new ResultPlugin(resolved)); //// RESOLVER //// for (const plugin of plugins) { if (typeof plugin === "function") { /** @type {function(this: Resolver, Resolver): void} */ (plugin).call(resolver, resolver); } else if (plugin) { plugin.apply(resolver); } } return resolver; };
我先把代码放上来。
一眼望上去400多行代码真多,实际上,他做了3件事情,非常简单
- createOptions(options); normalize options 参数
- 注册tapable钩子resolver.ensureHook("xxx");
- 注册插件
第一步我们直接跳过,不想讲normalize,因为实在没啥意思,无非就是字符串变成字符串数组,来回的变而已。
const resolver = customResolver? customResolver : new Resolver(fileSystem, normalizedOptions);
这行代码的关键是new Resolver(fileSystem, normalizedOptions);,因为我们不会传入自定义的customResolver
constructor(fileSystem, options) { this.fileSystem = fileSystem; this.options = options; /** @type {KnownHooks} */ this.hooks = { // 每执行一个插件都会调用 resolveStep: new SyncHook(["hook", "request"], "resolveStep"), // 没有找到具体文件或目录 noResolve: new SyncHook(["request", "error"], "noResolve"), // 开始解析 resolve: new AsyncSeriesBailHook( ["request", "resolveContext"], "resolve" ), // 解析完成 result: new AsyncSeriesHook(["result", "resolveContext"], "result") }; }
Resolver的构造函数就是这样,其实和compiler很像,都是有tapable的事件。
我们继续往下看 resolver.ensureHook("xxx");
ensureHook(name) { if (typeof name !== "string") { return name; } name = toCamelCase(name); if (/^before/.test(name)) { return /** @type {ResolveStepHook} */ ( this.ensureHook(name[6].toLowerCase() + name.slice(7)).withOptions({ stage: -10 }) ); } if (/^after/.test(name)) { return /** @type {ResolveStepHook} */ ( this.ensureHook(name[5].toLowerCase() + name.slice(6)).withOptions({ stage: 10 }) ); } const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name]; if (!hook) { /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name] = new AsyncSeriesBailHook( ["request", "resolveContext"], name ); return /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name]; } return hook; }
其实就是注册new AsyncSeriesBailHook(),如果带before前缀优先级会高,带after优先级会低,已经注册过,就走已经注册的。
我们接着往下看,下面是注册插件,我们直接走到最后一步,看起来和webpack没区别啊,没错,enhanced-resolve的设计形式和webpack就是非常像。
for (const plugin of plugins) { if (typeof plugin === "function") { (plugin).call(resolver, resolver); } else if (plugin) { plugin.apply(resolver); } }
ResolverFactory()我们就说完了,我们再看他上面的resolve方法为啥可以解析到具体的路径。
resolve(context, path, request, resolveContext, callback) { const obj = { context: context, //{} path: path, //路径 request: request //.a }; let yield_; let yieldCalled = false; let finishYield; if (typeof resolveContext.yield === "function") { const old = resolveContext.yield; yield_ = obj => { old(obj); yieldCalled = true; }; finishYield = result => { if (result) { (yield_)(result); } callback(null); }; } const message = `resolve ' ${request}' in ' ${path}'`; /** * @param {ResolveRequest} result result * @returns {void} */ 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 ); }; /** * @param {string[]} log logs * @returns {void} */ const finishWithoutResolve = log => { /** * @type {ErrorWithDetail} */ 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; /** @type {string[]} */ 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 /** @type {ResolveContextYield} */ (finishYield)( /** @type {ResolveRequest} */ (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 /** @type {ResolveContextYield} */ (finishYield)( /** @type {ResolveRequest} */ (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 /** @type {string[]} */ 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 /** @type {ResolveContextYield} */ (finishYield)( /** @type {ResolveRequest} */ (result) ); } return finishWithoutResolve(log); } ); } ); } }
看着resolve好像很多的样子,实际上,this.doResolve()就调用了这个方法而已。
doResolve(hook, request, message, resolveContext, callback) { const stackEntry = Resolver.createStackEntry(hook, request); /** @type {Set<string> | undefined} */ let 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]); } //默认没有传事件,所以不走 this.hooks.resolveStep.call(hook, request); if (hook.isUsed()) { const innerContext = createInnerContext( { log: resolveContext.log, yield: resolveContext.yield, fileDependencies: resolveContext.fileDependencies, contextDependencies: resolveContext.contextDependencies, missingDependencies: resolveContext.missingDependencies, stack: newStack }, message ); return hook.callAsync(request, innerContext, (err, result) => { if (err) return callback(err); if (result) return callback(null, result); callback(); }); } else { callback(); } }
doResolve其实就是调用了hook.callAsync方法。
我们第一次调用的是 doResolve(this.hooks.resolve)
hooks.resolve
resolve上面挂载了一个方法。
plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));
apply(resolver) { const target = resolver.ensureHook(this.target); resolver .getHook(this.source) .tapAsync("ParsePlugin", (request, resolveContext, callback) => { debugger // 调用 resolver 中的 parse 方法初步解析 const parsed = resolver.parse(/** @type {string} */ (request.request)); /** @type {ResolveRequest} */ // 合并成新的 obj 对象 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("/"); /** @type {ResolveRequest} */ const alternative = { ...obj, directory, request: obj.request + (obj.directory ? "/" : "") + (directory ? obj.fragment.slice(0, -1) : obj.fragment), fragment: "" }; 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); }); }
我直接说他做了什么吧,代码也很简单,它先解析parse request,然后 resolver.doResolve(target, obj, null, resolveContext, callback);,继续调用doResolve方法。
parse request 其实就是对request进行解析,得到query,fragment,internal,module,directory.(./src?query=122#id) fragement=#的内容,query=?的内容,有点像浏览器的url解析)
doResolve继续调用,那我们就知道是这段代码是怎么实现的了。
原来就是钩子里面继续调用钩子,先走resolve,走完parsed-resolve,然后依次,知道执行完毕。。。
hooks.parsed-resolve
parsed-resolve 有两个钩子
- plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
- plugins.push(new DescriptionFilePlugin("parsed-resolve",descriptionFiles,false,"described-resolve"));
NextPlugin 非常简单,就是啥也不执行,直接跳到下一个钩子,比如这里的described-resolve
DescriptionFilePlugin 做了哪些处理呢,其实也非常的简单,他会根据当前的路径去找package.json文件,如果这一级没有,就跳到上一级,代码就不列出来了,知道功能就好。
然后就走到described-resolve
hooks.described-resolve
只有NextPlugin这个,直接就跳到raw-resolve。
hooks.raw-resolve
我们的demo没有绑定插件,只有nextPlugin,所以跳到normal-resolve
hooks.normal-resolve
ConditionalPlugin
JoinRequestPlugin
ConditionalPlguin
一种条件判断的Plugin,全部代码如下所示,初始化会传入一个条件,判断是否满足这个条件,则可以继续下一个Plugin
for (const prop of keys) { if (request[prop] !== test[prop]) return callback(); } resolver.doResolve(target,request,...)
所以ConditionalPlugin的作用就是根据处理类型不同从而跳转到不同Plugin
很遗憾这里不匹配。
所以走到了JoinRequestPlugin
JoinRequestPlugin
改变path、relativePath、request
- path=path+request
- relativePath=relativePath+request
- request=undefined
本质是将目前请求的路径加上请求文件的名称,形成请求文件的绝对路径
相对路径变成了绝对路径。path="/usr/project/xxx/xxx/index.js" 举个这样的例子
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);
hooks.relative
执行到这里,其实一共就做了 1. parse requset 解析? # 等信息 2. 找到package.json 3. 把相对路径的requset 和path合并拿到绝对路径。
relative有两个 1.DescriptionFilePlugin 2. nextplugin
这俩哥们真眼熟啊,一个是处理package.json的,一个是跳转到下一个钩子的。
因为我们上边已经处理过了,所以大概率这里会直接callback,走了相当于没走。
没错,debugger代码确实直接callback。
所以直接下一个吧。
hooks.described-relative
有两个 ConditionalPlguin钩子 又是这位老哥
所以二者必走一个
plugins.push( new ConditionalPlugin( "described-relative", { directory: false }, null, true, "raw-file" ) ); plugins.push( new ConditionalPlugin( "described-relative", { fullySpecified: false }, "as directory", true, "directory" ) );
一看参数,我们一眼就知道走哪个了,一个是文件夹,一个是文件,我们这次处理的是文件,所以直接跳到raw-file。
hooks.raw-file
raw-file有5个钩子,一个TryNextPlugin,一个ConditionalPlugin,三个AppendPlugin
new ConditionalPlugin( "raw-file", { fullySpecified: true }, null, false,"file")
先走ConditionalPlugin,然后发现不匹配。
再走 plugins.push(new TryNextPlugin("raw-file", "no extension", "file"));
会直接去尝试下一个钩子,如果能走通就走下去,走不通就回来。
看名字 no extension 没有扩展名 进入file钩子
hooks.file
进入file钩子,发现只有nextPlugin,进入finalFile
hooks.finalFile
它内部只有FileExistsPlugin这个钩子
apply(resolver) { const target = resolver.ensureHook(this.target); const fs = resolver.fileSystem; resolver .getHook(this.source) .tapAsync("FileExistsPlugin", (request, resolveContext, callback) => { const file = request.path; if (!file) return callback(); fs.stat(file, (err, stat) => { if (err || !stat) { if (resolveContext.missingDependencies) resolveContext.missingDependencies.add(file); if (resolveContext.log) resolveContext.log(file + " doesn't exist"); return callback(); } if (!stat.isFile()) { if (resolveContext.missingDependencies) resolveContext.missingDependencies.add(file); if (resolveContext.log) resolveContext.log(file + " is not a file"); return callback(); } if (resolveContext.fileDependencies) resolveContext.fileDependencies.add(file); resolver.doResolve( target, request, "existing file: " + file, resolveContext, callback ); }); }); }
核心就是fs.stat(path),很明显,如果我们输入的是具体的./a.js,而不是./a,那就可以走下去,因为我们现在这条路径走下去的是无扩展名的,所以fs.stat返回error的信息。
所以只能原路返回,返回到hooks.raw-file
回到hooks.raw-file
剩下的就是AppendPlugin这个钩子,我们说有三个钩子,这是为什么呢。
extensions.forEach(item => {
plugins.push(new AppendPlugin("raw-file", item, "file"));
});
看名字就知道为啥了,因为一共有三个扩展。
extensions: [".json", ".js", ".ts"]
apply(resolver) { const target = resolver.ensureHook(this.target); resolver .getHook(this.source) .tapAsync("AppendPlugin", (request, resolveContext, callback) => { debugger /** @type {ResolveRequest} */ const obj = { ...request, path: request.path + this.appending, relativePath: request.relativePath && request.relativePath + this.appending }; resolver.doResolve( target, obj, this.appending, resolveContext, callback ); }); }
看一下这个函数,其实就是加别名而已。
还记得上面的流程吗,进入file,走到finalFile,但是这三个钟./a.js能找到文件,所以fs.stat是通过的,我们就可以往下走了。
new FileExistsPlugin("final-file", "existing-file")
hooks.existing-file
plugins.push(new SymlinkPlugin("existing-file", "existing-file"));
plugins.push(new NextPlugin("existing-file", "resolved"));
一个是 SymlinkPlugin , 一个老朋友NextPlugin
SymlinkPlugin软连接
代码不写了,就是利用fs.readlink拿到软连接地址,我们这里没有软连接。
所以走到 NextPlugin
hooks.resolved
plugins.push(new ResultPlugin(resolved)); 只有她了
apply(resolver) { this.source.tapAsync( "ResultPlugin", (request, resolverContext, callback) => { 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); } }); } ); }
我们的逻辑直接就走到callback(null,obj)
而这个callback就是,我们自己写的callback
(err, path, result) => {
console.log("createResolve path: ", path, result);
}
这就是ResolverFactory.
1. 包含常规前端面试题解析,和源码分析 2. 从0-1深入浅出理解前端相关技术栈