webpack 格式化模块工厂 第 二 节

这段代码为 Webpack 或类似工具的模块加载系统的核心部分,提供了强大的扩展性和配置灵活性。通过钩子机制,它允许开发者在模块解析、构建、加载、生成等各个阶段插入自定义逻辑,以满足不同的构建需求。

1. 构造函数参数

  • context: 模块解析的上下文(如当前工作目录)。
  • fs: 用于文件操作的文件系统对象。
  • resolverFactory: 解析器工厂,用于创建各种解析器。
  • options: 配置选项,包括默认规则、规则集、解析器选项、生成器选项等。
  • associatedObjectForCache: 用于缓存相关操作的对象。
  • layers: 是否启用层级功能。

2. 钩子定义

  • resolve: 解析模块的钩子,处理模块解析的结果。
  • resolveForScheme, resolveInScheme: 钩子映射,用于根据不同的 Scheme(资源解析方案)处理解析数据。
  • factorize: 用于模块的因式分解,创建模块实例。
  • beforeResolve, afterResolve: 在模块解析之前和之后执行的钩子。
  • createModule: 根据解析数据创建模块的钩子。
  • module: 用于修改已创建模块的钩子。
  • createParser, parser: 解析器相关的钩子。
  • createGenerator, generator: 生成器相关的钩子。
  • createModuleClass: 根据解析数据创建模块类的钩子。

3. 主要功能

  • 解析模块 :通过 resolve 钩子和 resolverFactory,对模块资源进行解析,支持多种方案(如不同的加载器、解析器等)。
  • 模块因式分解 :通过 factorize 钩子,根据解析的数据创建模块实例。
  • 解析与加载器的关系:构造函数内部会根据加载器规则,处理不同类型的加载器(如预加载器、普通加载器、后加载器等)。
  • 规则集编译与应用:编译并应用规则集(包括默认规则和用户自定义规则),用于处理不同资源的加载方式。
  • 资源与依赖管理:管理文件依赖、缺失依赖、上下文依赖等,确保在模块解析过程中正确处理。

4. 复杂的资源解析流程

  • 解析过程中考虑了不同的资源类型、请求路径、上下文路径等,并支持通过各种钩子进行扩展。
  • 钩子的执行顺序、阶段设置(如 stage)允许在不同的生命周期阶段执行特定的操作。
  • 针对每种资源类型(如 JavaScript、CSS 等),系统通过不同的钩子和规则进行相应的处理。

5. 模块创建与修改

  • 在模块解析完成后,使用 createModule 钩子根据解析数据创建模块实例。
  • 通过 module 钩子,允许在模块创建后进一步修改其属性或行为。
  • 通过 createModuleClass 钩子,支持根据解析数据创建不同的模块类。

6. 缓存机制

  • 使用 parserCachegeneratorCache 缓存解析器和生成器,优化模块的处理性能。
  • 通过 associatedObjectForCache 对象,缓存解析和生成过程中的数据,避免重复计算。

7. 异步与回调管理

  • 代码中大量使用异步钩子(如 AsyncSeriesBailHook)和回调函数(如 callback),确保在模块解析和创建过程中能够处理异步操作,保证流程的顺利进行。

8. 错误处理与异常管理

  • 代码中通过多次检查并返回错误(如解析失败、未提供必需的参数等)确保异常的处理,防止出现不符合预期的行为。
js 复制代码
	/**
	 * @param {object} param 参数
	 * @param {string=} param.context 上下文
	 * @param {InputFileSystem} param.fs 文件系统
	 * @param {ResolverFactory} param.resolverFactory 解析器工厂
	 * @param {ModuleOptions} param.options 模块选项
	 * @param {object} param.associatedObjectForCache 缓存将附加到的对象
	 * @param {boolean=} param.layers 启用层级
	 */
	constructor({
		context,
		fs,
		resolverFactory,
		options,
		associatedObjectForCache,
		layers = false
	}) {
		super(); // 调用父类构造函数,初始化基类
		this.hooks = Object.freeze({
			/** 
			 * @type {AsyncSeriesBailHook<[ResolveData], Module | false | void>}
			 * 用于解析模块的钩子。可以返回一个 Module 实例,或者返回 false 表示忽略该解析,或者返回 void。
			 */
			resolve: new AsyncSeriesBailHook(["resolveData"]),

			/** 
			 * @type {HookMap<AsyncSeriesBailHook<[ResourceDataWithData, ResolveData], true | void>>} 
			 * 钩子映射,针对不同的 scheme 解析资源数据。返回 true 或 void。
			 */
			resolveForScheme: new HookMap(
				() => new AsyncSeriesBailHook(["resourceData", "resolveData"])
			),

			/** 
			 * @type {HookMap<AsyncSeriesBailHook<[ResourceDataWithData, ResolveData], true | void>>} 
			 * 与 `resolveForScheme` 相似,但用于特定的 scheme 上下文。
			 */
			resolveInScheme: new HookMap(
				() => new AsyncSeriesBailHook(["resourceData", "resolveData"])
			),

			/** 
			 * @type {AsyncSeriesBailHook<[ResolveData], Module | undefined>} 
			 * 用于模块的因式分解钩子。返回 Module 实例或 undefined。
			 */
			factorize: new AsyncSeriesBailHook(["resolveData"]),

			/** 
			 * @type {AsyncSeriesBailHook<[ResolveData], false | void>} 
			 * 在解析模块之前调用的钩子。可以返回 false 以阻止解析过程。
			 */
			beforeResolve: new AsyncSeriesBailHook(["resolveData"]),

			/** 
			 * @type {AsyncSeriesBailHook<[ResolveData], false | void>} 
			 * 在解析模块之后调用的钩子。可以返回 false 以停止进一步的处理。
			 */
			afterResolve: new AsyncSeriesBailHook(["resolveData"]),

			/** 
			 * @type {AsyncSeriesBailHook<[ResolveData["createData"], ResolveData], Module | void>} 
			 * 用于根据解析数据创建模块的钩子。
			 */
			createModule: new AsyncSeriesBailHook(["createData", "resolveData"]),

			/** 
			 * @type {SyncWaterfallHook<[Module, ResolveData["createData"], ResolveData]>} 
			 * 用于修改已创建模块的钩子,模块已解析并传递了工厂数据。
			 */
			module: new SyncWaterfallHook(["module", "createData", "resolveData"]),

			/** 
			 * @type {HookMap<SyncBailHook<[ParserOptions], Parser | void>>} 
			 * 钩子映射,用于创建解析器。如果解析器创建失败,则返回 void。
			 */
			createParser: new HookMap(() => new SyncBailHook(["parserOptions"])),

			/** 
			 * @type {HookMap<SyncBailHook<[TODO, ParserOptions], void>>} 
			 * 用于修改解析器的钩子,解析器创建之后执行。
			 */
			parser: new HookMap(() => new SyncHook(["parser", "parserOptions"])),

			/** 
			 * @type {HookMap<SyncBailHook<[GeneratorOptions], Generator | void>>} 
			 * 钩子映射,用于创建生成器。返回生成器实例或 void 表示没有生成。
			 */
			createGenerator: new HookMap(
				() => new SyncBailHook(["generatorOptions"])
			),

			/** 
			 * @type {HookMap<SyncBailHook<[TODO, GeneratorOptions], void>>} 
			 * 用于修改生成器的钩子,生成器创建之后执行。
			 */
			generator: new HookMap(
				() => new SyncHook(["generator", "generatorOptions"])
			),

			/** 
			 * @type {HookMap<SyncBailHook<[TODO, ResolveData], Module | void>>} 
			 * 钩子映射,用于根据解析数据创建模块类。
			 */
			createModuleClass: new HookMap(
				() => new SyncBailHook(["createData", "resolveData"])
			)
		});

		this.resolverFactory = resolverFactory; // 初始化模块解析器工厂
		this.ruleSet = ruleSetCompiler.compile([  // 编译规则集,包括默认规则和用户定义的规则
			{
				rules: options.defaultRules
			},
			{
				rules: options.rules
			}
		]);
		this.context = context || ""; // 设置模块解析的上下文(目录)
		this.fs = fs; // 设置文件系统,用于文件解析
		this._globalParserOptions = options.parser; // 存储全局的解析器选项
		this._globalGeneratorOptions = options.generator; // 存储全局的生成器选项

		/** @type {Map<string, WeakMap<object, Parser>>} */
		this.parserCache = new Map(); // 缓存不同类型资源的解析器
		/** @type {Map<string, WeakMap<object, Generator>>} */
		this.generatorCache = new Map(); // 缓存不同类型资源的生成器
		/** @type {Set<Module>} */
		this._restoredUnsafeCacheEntries = new Set(); // 存储已恢复的模块

		// 绑定并缓存解析资源
		const cacheParseResource = parseResource.bindCache(
			associatedObjectForCache
		);
		const cachedParseResourceWithoutFragment =
			parseResourceWithoutFragment.bindCache(associatedObjectForCache);
		this._parseResourceWithoutFragment = cachedParseResourceWithoutFragment;

		// 因式分解钩子,通过解析数据创建模块
		this.hooks.factorize.tapAsync(
			{
				name: "NormalModuleFactory",  // 为此钩子命名,便于识别
				stage: 100  // 设置钩子链中的执行阶段
			},
			(resolveData, callback) => {
				// 首先通过 `resolve` 钩子解析数据
				this.hooks.resolve.callAsync(resolveData, (err, result) => {
					if (err) return callback(err);  // 处理解析时的错误

					// 如果返回值是 false,表示忽略该解析
					if (result === false) return callback();

					// 如果返回值是 Module 实例,立即返回该模块
					if (result instanceof Module) return callback(null, result);

					// 如果返回的是对象(但不是 Module),则抛出弃用的错误
					if (typeof result === "object")
						throw new Error(
							`${deprecationChangedHookMessage(
								"resolve",
								this.hooks.resolve
							)} Returning a Module object will result in this module used as result.`
						);

					// 调用 `afterResolve` 钩子,在解析后执行
					this.hooks.afterResolve.callAsync(resolveData, (err, result) => {
						if (err) return callback(err);  // 处理 afterResolve 钩子中的错误

						// 如果返回值是对象,表示弃用,抛出错误
						if (typeof result === "object")
							throw new Error(
								deprecationChangedHookMessage(
									"afterResolve",
									this.hooks.afterResolve
								)
							);

						// 如果返回值是 false,表示忽略该解析
						if (result === false) return callback();

						const createData = resolveData.createData;

						// 使用 `createModule` 钩子创建模块
						this.hooks.createModule.callAsync(
							createData,
							resolveData,
							(err, createdModule) => {
								if (!createdModule) {
									if (!resolveData.request) {
										return callback(new Error("Empty dependency (no request)"));
									}

									// 如果未创建模块,使用模块类创建
									createdModule = this.hooks.createModuleClass
										.for(
											/** @type {ModuleSettings} */
											(createData.settings).type
										)
										.call(createData, resolveData);

									// 如果没有模块类,则默认使用 NormalModule 创建模块
									if (!createdModule) {
										createdModule = /** @type {Module} */ (
											new NormalModule(
												/** @type {NormalModuleCreateData} */
												(createData)
											)
										);
									}
								}

								// 应用 `module` 钩子,修改模块后再返回
								createdModule = this.hooks.module.call(
									createdModule,
									createData,
									resolveData
								);

								// 最后返回创建的模块
								return callback(null, createdModule);
							}
						);
					});
				});
			}
		);
		// 注册 resolve 钩子,处理模块资源解析
		this.hooks.resolve.tapAsync(
			{
				name: "NormalModuleFactory", // 钩子的名称
				stage: 100 // 钩子的执行阶段,值越小,越早执行
			},
			(data, callback) => {
				const {
					contextInfo,
					context,
					dependencies,
					dependencyType,
					request,
					assertions,
					resolveOptions,
					fileDependencies,
					missingDependencies,
					contextDependencies
				} = data; // 解构获取传入的数据

				const loaderResolver = this.getResolver("loader"); // 获取 loader 解析器

				let matchResourceData; // 匹配的资源数据
				let unresolvedResource; // 未解析的资源
				let elements; // loader 元素
				let noPreAutoLoaders = false; // 是否没有预加载器
				let noAutoLoaders = false; // 是否没有加载器
				let noPrePostAutoLoaders = false; // 是否没有预后加载器

				const contextScheme = getScheme(context); // 获取上下文的 Scheme
				let scheme = getScheme(request); // 获取请求的 Scheme

				if (!scheme) {
					let requestWithoutMatchResource = request;
					const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request); // 匹配资源

					if (matchResourceMatch) {
						let matchResource = matchResourceMatch[1];
						if (matchResource.charCodeAt(0) === 46) { // 检查是否以 . 或 ../ 开头
							const secondChar = matchResource.charCodeAt(1);
							if (secondChar === 47 || (secondChar === 46 && matchResource.charCodeAt(2) === 47)) {
								matchResource = join(this.fs, context, matchResource); // 处理相对路径
							}
						}
						matchResourceData = {
							resource: matchResource,
							...cacheParseResource(matchResource)
						};
						requestWithoutMatchResource = request.slice(matchResourceMatch[0].length); // 去除匹配到的资源部分
					}

					scheme = getScheme(requestWithoutMatchResource);

					if (!scheme && !contextScheme) {
						const firstChar = requestWithoutMatchResource.charCodeAt(0);
						const secondChar = requestWithoutMatchResource.charCodeAt(1);
						noPreAutoLoaders = firstChar === 45 && secondChar === 33; // startsWith "-!"
						noAutoLoaders = noPreAutoLoaders || firstChar === 33; // startsWith "!"
						noPrePostAutoLoaders = firstChar === 33 && secondChar === 33; // startsWith "!!"
						const rawElements = requestWithoutMatchResource.slice(
							noPreAutoLoaders || noPrePostAutoLoaders ? 2 : noAutoLoaders ? 1 : 0
						).split(/!+/);
						unresolvedResource = rawElements.pop(); // 获取未解析的资源
						elements = rawElements.map(el => {
							const { path, query } = cachedParseResourceWithoutFragment(el); // 解析 loader 的路径和查询字符串
							return {
								loader: path,
								options: query ? query.slice(1) : undefined
							};
						});
						scheme = getScheme(unresolvedResource);
					} else {
						unresolvedResource = requestWithoutMatchResource;
						elements = EMPTY_ELEMENTS;
					}
				} else {
					unresolvedResource = request;
					elements = EMPTY_ELEMENTS;
				}

				const resolveContext = {
					fileDependencies,
					missingDependencies,
					contextDependencies
				};

				let resourceData;

				let loaders;

				const continueCallback = needCalls(2, err => {
					if (err) return callback(err);

					try {
						// 处理 loader 的配置
						for (const item of loaders) {
							if (typeof item.options === "string" && item.options[0] === "?") {
								const ident = item.options.slice(1);
								if (ident === "[[missing ident]]") {
									throw new Error(
										"No ident is provided by referenced loader. When using a function for Rule.use in config you need to provide an 'ident' property for referenced loader options."
									);
								}
								item.options = this.ruleSet.references.get(ident);
								if (item.options === undefined) {
									throw new Error("Invalid ident is provided by referenced loader");
								}
								item.ident = ident;
							}
						}
					} catch (identErr) {
						return callback(identErr);
					}

					if (!resourceData) {
						return callback(null, dependencies[0].createIgnoredModule(context)); // 如果没有资源数据,返回忽略模块
					}

					const userRequest = (matchResourceData !== undefined ? `${matchResourceData.resource}!=!` : "") +
						stringifyLoadersAndResource(loaders, resourceData.resource);

					const settings = {};
					const useLoadersPost = [];
					const useLoaders = [];
					const useLoadersPre = [];

					let resource;
					let match;

					if (matchResourceData && typeof (resource = matchResourceData.resource) === "string" && (match = /\.webpack\[([^\]]+)\]$/.exec(resource))) {
						settings.type = match[1];
						matchResourceData.resource = matchResourceData.resource.slice(0, -settings.type.length - 10);
					} else {
						settings.type = JAVASCRIPT_MODULE_TYPE_AUTO;
						const resourceDataForRules = matchResourceData || resourceData;
						const result = this.ruleSet.exec({
							resource: resourceDataForRules.path,
							realResource: resourceData.path,
							resourceQuery: resourceDataForRules.query,
							resourceFragment: resourceDataForRules.fragment,
							scheme,
							assertions,
							mimetype: matchResourceData ? "" : resourceData.data.mimetype || "",
							dependency: dependencyType,
							descriptionData: matchResourceData ? undefined : resourceData.data.descriptionFileData,
							issuer: contextInfo.issuer,
							compiler: contextInfo.compiler,
							issuerLayer: contextInfo.issuerLayer || ""
						});

						for (const r of result) {
							if (r.type === "use") {
								if (!noAutoLoaders && !noPrePostAutoLoaders) {
									useLoaders.push(r.value);
								}
							} else if (r.type === "use-post") {
								if (!noPrePostAutoLoaders) {
									useLoadersPost.push(r.value);
								}
							} else if (r.type === "use-pre") {
								if (!noPreAutoLoaders && !noPrePostAutoLoaders) {
									useLoadersPre.push(r.value);
								}
							} else if (typeof r.value === "object" && r.value !== null && typeof settings[r.type] === "object" && settings[r.type] !== null) {
								settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
							} else {
								settings[r.type] = r.value;
							}
						}
					}

					let postLoaders;
					let normalLoaders;
					let preLoaders;

					const continueCallback = needCalls(3, err => {
						if (err) return callback(err);

						const allLoaders = postLoaders;
						if (matchResourceData === undefined) {
							loaders.forEach(loader => allLoaders.push(loader));
							normalLoaders.forEach(loader => allLoaders.push(loader));
						} else {
							normalLoaders.forEach(loader => allLoaders.push(loader));
							loaders.forEach(loader => allLoaders.push(loader));
						}
						preLoaders.forEach(loader => allLoaders.push(loader));

						const type = settings.type;
						const resolveOptions = settings.resolve;
						const layer = settings.layer;

						if (layer !== undefined && !layers) {
							return callback(new Error("'Rule.layer' is only allowed when 'experiments.layers' is enabled"));
						}

						try {
							Object.assign(data.createData, {
								layer: layer === undefined ? contextInfo.issuerLayer || null : layer,
								request: stringifyLoadersAndResource(allLoaders, resourceData.resource),
								userRequest,
								rawRequest: request,
								loaders: allLoaders,
								resource: resourceData.resource,
								context: resourceData.context || getContext(resourceData.resource),
								matchResource: matchResourceData ? matchResourceData.resource : undefined,
								resourceResolveData: resourceData.data,
								settings,
								type,
								parser: this.getParser(type, settings.parser),
								parserOptions: settings.parser,
								generator: this.getGenerator(type, settings.generator),
								generatorOptions: settings.generator,
								resolveOptions
							});
						} catch (createDataErr) {
							return callback(createDataErr);
						}
						callback();
					});

					this.resolveRequestArray(contextInfo, this.context, useLoadersPost, loaderResolver, resolveContext, (err, result) => {
						postLoaders = result;
						continueCallback(err);
					});

					this.resolveRequestArray(contextInfo, this.context, useLoaders, loaderResolver, resolveContext, (err, result) => {
						normalLoaders = result;
						continueCallback(err);
					});

					this.resolveRequestArray(contextInfo, this.context, useLoadersPre, loaderResolver, resolveContext, (err, result) => {
						preLoaders = result;
						continueCallback(err);
					});
				});

				this.resolveRequestArray(contextInfo, contextScheme ? this.context : context, elements, loaderResolver, resolveContext, (err, result) => {
					if (err) return continueCallback(err);
					loaders = result;
					continueCallback();
				});

				const defaultResolve = context => {
					if (/^($|\?)/.test(unresolvedResource)) {
						resourceData = {
							resource: unresolvedResource,
							data: {},
							...cacheParseResource(unresolvedResource)
						};
						continueCallback();
					} else {
						const normalResolver = this.getResolver("normal", dependencyType ? cachedSetProperty(resolveOptions || EMPTY_RESOLVE_OPTIONS, "dependencyType", dependencyType) : resolveOptions);
						this.resolveResource(
							contextInfo,
							context,
							unresolvedResource,
							normalResolver,
							resolveContext,
							(err, _resolvedResource, resolvedResourceResolveData) => {
								if (err) return continueCallback(err);
								if (_resolvedResource !== false) {
									const resolvedResource = _resolvedResource;
									resourceData = {
										resource: resolvedResource,
										data: resolvedResourceResolveData,
										...cacheParseResource(resolvedResource)
									};
								}
								continueCallback();
							}
						);
					}
				};

				if (scheme) {
					resourceData = {
						resource: unresolvedResource,
						data: {},
						path: undefined,
						query: undefined,
						fragment: undefined,
						context: undefined
					};
					this.hooks.resolveForScheme.for(scheme).callAsync(resourceData, data, err => {
						if (err) return continueCallback(err);
						continueCallback();
					});
				} else if (contextScheme) {
					resourceData = {
						resource: unresolvedResource,
						data: {},
						path: undefined,
						query: undefined,
						fragment: undefined,
						context: undefined
					};
					this.hooks.resolveInScheme.for(contextScheme).callAsync(resourceData, data, (err, handled) => {
						if (err) return continueCallback(err);
						if (!handled) return defaultResolve(this.context);
						continueCallback();
					});
				} else defaultResolve(context);
			}
		);

	}
相关推荐
WindrunnerMax3 分钟前
深感一无所长,准备试着从零开始写个富文本编辑器
前端·javascript·github
Richard20129 分钟前
Linux Command Recap
linux·前端
尖椒土豆sss9 分钟前
原子化 css 框架:Tailwind Css 入门学习
前端·css·postcss
iOS阿玮26 分钟前
2025年第一季度3.2f求助排名第一,你还敢违规操作么?
前端·app·apple
gyratesky36 分钟前
用4种方法实现内发光的多边形区域
前端·数据可视化
敲代码的彭于晏1 小时前
前端上传与下载基础:Blob、File与ArrayBuffer详解
前端
緑水長流1 小时前
什么是Promise?什么是async和await?
前端·javascript·vue.js
Mintopia1 小时前
Three.js 相机(Camera)的使用详解
前端·javascript·three.js
wordbaby1 小时前
PC 屏幕自适应的流行方案解析
前端