共计 10157 个字符,预计需要花费 26 分钟才能阅读完成。
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 开始前面的构建环节。