Webpack初始化
const webpack = require("webpack");
const config = require("./webpack.config");
const compiler = webpack(config);
compiler.run();
虽然大部分情况都在用cli或者dev-server跑webpack,它们能提供很多命令,接收参数,配置不同的npm script去跑不同的config等。但它们最终会跑以上代码的时候,开始进行打包的工作。当然,监听文件改动是用compiler.watch
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)
}
//...
applyWebpackOptionsBaseDefaults
和applyWebpackOptionsDefaults
都是给没设置的基本配置加上默认值,先执行前面的是因为需要抛出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
Compilation.taps
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。
//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开始后面的构建环节。