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件事情,非常简单

  1. createOptions(options); normalize options 参数
  2. 注册tapable钩子resolver.ensureHook("xxx");
  3. 注册插件

第一步我们直接跳过,不想讲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 有两个钩子

  1. plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
  2. 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

改变pathrelativePathrequest

  • 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深入浅出理解前端相关技术栈

全部评论

相关推荐

评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客企业服务