Skip to content

webpack5 源码详解 - 初始化 #16

@Hazlank

Description

@Hazlank

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开始后面的构建环节。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions