Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack5 源码详解 - 初始化 #16

Open
Hazlank opened this issue Apr 10, 2022 · 0 comments
Open

webpack5 源码详解 - 初始化 #16

Hazlank opened this issue Apr 10, 2022 · 0 comments
Labels

Comments

@Hazlank
Copy link
Owner

Hazlank commented Apr 10, 2022

Webpack初始化

const webpack = require("webpack");
const config = require("./webpack.config");

const compiler = webpack(config);
compiler.run();

虽然大部分情况都在用cli或者dev-server跑webpack,它们能提供很多命令,接收参数,配置不同的npm script去跑不同的config等。但它们最终会跑以上代码,开始进行打包的工作。

webpack(config)

首先执行const compiler = webpack(config)

webpack.js

const webpack =  (
	(options, callback) => {
		  //...
		  const webpackOptions = (options);
			//构建compiler
		  compiler = createCompiler(webpackOptions);
		  //...	
		  return { compiler };
	}
);

const createCompiler = rawOptions => {
	//将没处理过的options进行处理
	const options = getNormalizedWebpackOptions(rawOptions);

	//设置default值
	applyWebpackOptionsBaseDefaults(options);
	const compiler = new Compiler(options.context, options);
	
	//NodeEnvironmentPlugin会引入独立库(enhanced-resolve, NodeWatchFileSystem)来增强Node模块
	new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);

	//注册外部plugin
	if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else {
				plugin.apply(compiler);
			}
		}
	}
	applyWebpackOptionsDefaults(options);
	//...
	new WebpackOptionsApply().process(options, compiler);
	return compiler;
};

webpack会拿到options,并且调用createCompiler(options)生成compiler实例并返回。

getNormalizedWebpackOptions会先处理options,传进来的options并不是拿来就用,有许多配置需要处理。

//getNormalizedWebpackOptions.js
const getNormalizedWebpackOptions = config => {
	return {
		cache: optionalNestedConfig(config.cache, cache => {
			if (cache === false) return false;
			if (cache === true) {
				return {
					type: "memory",
					maxGenerations: undefined
				};
			}
			switch (cache.type) {
				case "filesystem":
					return {
						//....
					};
				case undefined:
				case "memory":
					return {
						type: "memory",
						maxGenerations: cache.maxGenerations
					};
				default:
					throw new Error(`Not implemented cache.type ${cache.type}`);
			}
		}),
		devServer: optionalNestedConfig(config.devServer, devServer => ({
			...devServer
		})),
		entry:
			config.entry === undefined
				? { main: {} }
				: typeof config.entry === "function"
				? (
						fn => () =>
							Promise.resolve().then(fn).then(getNormalizedEntryStatic)
				  )(config.entry)
				: getNormalizedEntryStatic(config.entry)
		}
		//...

applyWebpackOptionsBaseDefaultsapplyWebpackOptionsDefaults都是给没设置的基本配置加上默认值,先执行前面的是因为需要抛出options给下面的NodeEnvironmentPlugin使用

//如果没有该属性就设置工厂函数的返回值
const F = (obj, prop, factory) => {
	if (obj[prop] === undefined) {
		obj[prop] = factory();
	}
};

//如果没有该属性就进行设置
const D = (obj, prop, value) => {
	if (obj[prop] === undefined) {
		obj[prop] = value;
	}
};

const applyWebpackOptionsBaseDefaults = options => {
	//...
	F(infrastructureLogging, "stream", () => process.stderr);
	D(infrastructureLogging, "level", "info");
	D(infrastructureLogging, "debug", false);
	D(infrastructureLogging, "colors", tty);
	D(infrastructureLogging, "appendOnly", !tty);
};

const applyWebpackOptionsDefaults = options => {
	F(options, "context", () => process.cwd());
	F(options, "target", () => {
		return getDefaultTarget(options.context);
	});
	//...
	F(options, "devtool", () => (development ? "eval" : false));
	D(options, "watch", false);
	//...
}

处理完options之后就会实例化生成Compiler对象,这时候就可以往Compiler注入插件。它们会执行所有options.plugins里的apply方法,写过插件的人都知道,编写插件需要暴露apply函数,并且得到Compiler对象往compiler.hooks里注入钩子, 如果不清楚hook的用法,建议读我写的这篇文章

最后调用new WebpackOptionsApply().process(options, compiler)方法,为该有的配置去注册相应的插件。初始化Compiler的工作就完成了

//WebpackOptionsApply.js

//....
if (options.externals) {
	const ExternalsPlugin = require("./ExternalsPlugin");
	new ExternalsPlugin(options.externalsType, options.externals).apply(
		compiler
	);
}

if (options.optimization.usedExports) {
	const FlagDependencyUsagePlugin = require("./FlagDependencyUsagePlugin");
	new FlagDependencyUsagePlugin(
		options.optimization.usedExports === "global"
	).apply(compiler);
}

//....

compiler.run()

run(callback) {
	//...
	const run = () => {
			//...
			this.compile(onCompiled);
		});
	};
	
	run()
}

//....

compile(callback) {
	//获取生成Compilation需要的参数
	const params = this.newCompilationParams();
	this.hooks.beforeCompile.callAsync(params, err => {
		if (err) return callback(err);
		
		this.hooks.compile.call(params);
		
		//生成compilation
		const compilation = this.newCompilation(params);

		const logger = compilation.getLogger("webpack.Compiler");

		logger.time("make hook");
		this.hooks.make.callAsync(compilation, err => {
			//...
		});
	});
}

run方法里会调用一些钩子与记录信息,在这里并不重要,主要在于this.compile(onCompiled),onCompiled是最终seal阶段之后的会执行的回调。

生成Compilation

compile函数首先会生成params给实例化Compilation作为参数

newCompilationParams() {
	const params = {
		normalModuleFactory: this.createNormalModuleFactory(),
		contextModuleFactory: this.createContextModuleFactory()
	};
	return params;
}

const params = this.newCompilationParams();

normalModuleFactory会生成normalModule,webpack里的模块就是normalModule对象。contextModuleFactory会生成contextModule,它是为了处理(require.context引用进来的模块。

createCompilation(params) {
	this._cleanupLastCompilation();
	//根据参数实例化Compilation
	return (this._lastCompilation = new Compilation(this, params));
}

newCompilation(params) {
	//实例化Compilation
	const compilation = this.createCompilation(params);
	compilation.name = this.name;
	compilation.records = this.records;
	//注册钩子
	this.hooks.thisCompilation.call(compilation, params);
	//注册钩子
	this.hooks.compilation.call(compilation, params);
	return compilation;
}

newCompilation会调用createCompilation实例化Compilation对象,并且调用钩子。

因为这时候compiler对象已经有了compilation和normalModule,所以可以传递给插件使用它们 , 或给它们的钩子注入函数实现相关功能。

在thisCompilation钩子里的插件有九个,compilation钩子甚至有四十几个,它们都是些内部插件。

thisCompilation.taps

img

Compilation.taps

img

ruleSetCompiler

在实例化normalModuleFactory的时候还会对rule进行处理,可以为之后处理模块的时候判断使用什么loader

//normalModuleFactory.js

const ruleSetCompiler = new RuleSetCompiler([
	new BasicMatcherRulePlugin("test", "resource"),
	new BasicMatcherRulePlugin("scheme"),
	new BasicMatcherRulePlugin("mimetype"),
	new BasicMatcherRulePlugin("dependency"),
	new BasicMatcherRulePlugin("include", "resource"),
	new BasicMatcherRulePlugin("exclude", "resource", true),
	//...
]);

class normalModuleFactory {
	construator() {
		//...
		this.ruleSet = ruleSetCompiler.compile([
			{
				rules: options.defaultRules
			},
			{
				rules: options.rules
			}
		]);
		//...
	}
}

实例化ruleSetCompiler的时候会把自己作为参数给插件用。然后调用compile,将options.rules和options.defaultRules传入进去。defaultRules是在applyWebpackOptionsDefaults的时候生成的默认rules。

img

//RuleSetCompiler.js

class RuleSetCompiler {
	constructor(plugins) {
		this.hooks = Object.freeze({
			//...
		});
		if (plugins) {
			for (const plugin of plugins) {
				plugin.apply(this);
			}
		}
	}

	compile(ruleSet) {
		const refs = new Map();
		//编译rules
		const rules = this.compileRules("ruleSet", ruleSet, refs);

		//用于根据rule抛出对应的loader
		const execRule = (data, rule, effects) => {
			//..
		};

		return {
			references: refs,
			exec: data => {
				/** @type {Effect[]} */
				const effects = [];
				for (const rule of rules) {
					execRule(data, rule, effects);
				}
				return effects;
			}
		};
	}

	compileRules(path, rules, refs) {
		return rules.map((rule, i) =>
			//递归options.rules和options.defaultRules
			this.compileRule(`${path}[${i}]`, rule, refs)
		);
	}

	compileRule(path, rule, refs) {
		//...
	}
	

RuleSetCompiler.compile会调用compileRules("ruleSet", ruleSet, refs)拼凑path并递归进行处理。

第一次调用compileRules传进来的path为ruleSet,ruleSet是上面包含options.rules和options.defaultRules的数组 。

	compileRule = (path, rule, refs)  => {
		const unhandledProperties = new Set(
			Object.keys(rule).filter(key => rule[key] !== undefined)
		);

		/** @type {CompiledRule} */
		const compiledRule = {
			conditions: [],
			effects: [],
			rules: undefined,
			oneOf: undefined
		};

		//判断是否含有rules的某些参数以加入到compiledRule里
		this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);

		//判断key是否包含rules
		if (unhandledProperties.has("rules")) {
			unhandledProperties.delete("rules");
			const rules = rule.rules;
			if (!Array.isArray(rules))
				throw this.error(path, rules, "Rule.rules must be an array of rules");
			compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs);
		}
		
		//判断key是否包含oneOf
		if (unhandledProperties.has("oneOf")) {
			unhandledProperties.delete("oneOf");
			const oneOf = rule.oneOf;
			if (!Array.isArray(oneOf))
				throw this.error(path, oneOf, "Rule.oneOf must be an array of rules");
			compiledRule.oneOf = this.compileRules(`${path}.oneOf`, oneOf, refs);
		}

		if (unhandledProperties.size > 0) {
			throw this.error(
				path,
				rule,
				`Properties ${Array.from(unhandledProperties).join(", ")} are unknown`
			);
		}

		return compiledRule;
	}

compileRule会递归处理所有含有rules和oneOf的嵌套对象,比如传进来的path为rulSet[0],所以会取第一个对象为options.defaultRules。然后unhandledProperties会取出数组每个Object keys,options.defaultRules对象的key为'rules',所以满足unhandledProperties.has("rules")。会调用compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs)递归defaultRules数组

第二次递归path为rulSet[0].rules[0],然后会调用this.hooks.rule.call处理defaultRules里的每个规则。钩子会调用之前注册的BasicMatcherRulePlugin对rules的属性生成不同的conditions

class BasicMatcherRulePlugin {
	constructor(ruleProperty, dataProperty, invert) {
		this.ruleProperty = ruleProperty;
		this.dataProperty = dataProperty || ruleProperty;
		this.invert = invert || false;
	}
	apply(ruleSetCompiler) {
		ruleSetCompiler.hooks.rule.tap(
			"BasicMatcherRulePlugin",
			(path, rule, unhandledProperties, result) => {
				if (unhandledProperties.has(this.ruleProperty)) {
					unhandledProperties.delete(this.ruleProperty);
					const value = rule[this.ruleProperty];
					//生成Condition
					const condition = ruleSetCompiler.compileCondition(
						`${path}.${this.ruleProperty}`,
						value
					);
					const fn = condition.fn;
					//添加到compileRule里
					result.conditions.push({
						property: this.dataProperty,
						matchWhenEmpty: this.invert
							? !condition.matchWhenEmpty
							: condition.matchWhenEmpty,
						fn: this.invert ? v => !fn(v) : fn
					});
				}
			}
		);
	}
}

比如rule为{ test: /\.js/ , use: babel-loader },插件new BasicMatcherRulePlugin("test", "resource")会处理所有包含test属性的rules,会生成如下:

[
	{
		conditions: [
			{ property: "resource", matchWhenEmpty: false, fn:v => typeof v === "string" && condition.test(v) },
			{ property: "resource", matchWhenEmpty: true, fn:v => !fn(v) }
		],
		effects: [{ type: "use", value: { loader: "babel-loader" } }]
	}
];

condition就是/\.js/,对于之后调用exec解析js模块就会抛出babel-loader。处理完所有的rules后,RuleSetCompiler.compile会返回如下对象

{
	references: refs,
	//exec会对模块名执行符合的condition并抛出effects数组,effects包含对应的loader信息
	exec: data => {
		/** @type {Effect[]} */
		const effects = [];
		for (const rule of rules) {
			execRule(data, rule, effects);
		}
		return effects;
	}
};

之后只要执行RuleSetCompiler.exec()就能返回相对应的loader,使用方法如下

this.ruleSet.exec({
	resource: resourceDataForRules.path,		//资源的绝对路径
	realResource: resourceData.path,
	resourceQuery: resourceDataForRules.query,		//资源携带的query string
	resourceFragment: resourceDataForRules.fragment,	
	scheme,		//URL方案 ,列如,data,file
	assertions,
	mimetype: matchResourceData
		? ""
		: resourceData.data.mimetype || "",   // mimetype
	dependency: dependencyType,			// 依赖类型
	descriptionData: matchResourceData		//	描述文件数据,比如package.json
		? undefined
		: resourceData.data.descriptionFileData,
	issuer: contextInfo.issuer,						//发起请求的模块
	compiler: contextInfo.compiler,				//当前webpack的compiler
	issuerLayer: contextInfo.issuerLayer || ""
});

到这里,生成compilation的工作就做完了,继续Compiler的钩子流程,之后就是调用this.hooks.make.callAsync方法了,开始从入口构建模块。之后会有很多async hook的代码,因为是异步的原因所以会有callback hell问题,阅读起来特别恶心,而且因为async hook里可以是setTimeout,源码实现也并没有返回promise,所以也不能使用async await解决回调问题

总结

以上就是一些初始化的代码,处理options,rules,注册插件,实例化normalModule,compilation对象,调用钩子传递对象给插件使用等。所有的工作做完了,会调用make hook开始后面的构建环节。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant