tapable在webpack主流程中的应用

前一阵子研究了tapable的实现后,就一直在学习webpack的源码。虽然事先有预料tapablewebpack中是非常核心的存在,但过了一遍主流程后,发现比运用的想象中更普遍。可以说整个webpack就是用tapable串联起来的。这篇文章主要是记录webpack 主流程中的tapable运用,并不会涉及到非常细节的代码实现,因为我也没看完囧

webpack中的绝大部分功能都是通过注册事件监听实现的,如果把整个打包流程看成是一辆从起点到终点运行的火车,那么每个事件就相当于一个个途中站点,事件监听函数类似于需要在对应站点下车的行人,行人就是我们的一个个Plugin,注册事件监听类似于在 A 站点上车并且想要在 B 站点下车,事件触发类似于火车到达了 B 站点,行人下车该干嘛干嘛,Plugin也就在这时开始干活。webpack中涉及的站点和行人都非常多,想要挨个弄清楚需要花费很多精力,我自己也没有怎么研究,不过了解了主流程后,就可以按兴趣针对性学习了,此时会省很多时间。

流程图

整理了一个主流程图:images/webpack/webpack-main-procedure.jpg

  • 蓝色的节点表示触发了一个事件
  • 黑色的节点表示调用了一个方法
  • 竖直的泳道表示节点所属的对象或文件名
  • 水平的泳道表示打包流程所属的阶段,整个过程可以分为 3 个阶段
    1. 编译准备阶段,主要是准备好各种配置项,初始化CompilerCompilation对象
    2. 生成moduleschunks,可能一些人对二者的关系不是很清楚,module可以理解为一个打包产物的最小单元,比如一个js文件、一张图片;而一个chunk对应一个最终生成的文件,内部可能包含多个modulechunkmodule是一对多的关系
    3. 生成打包产物,也就是一个个文件

上述只画了一部分的节点,实际上涉及到的类和节点要多得多,不过在清楚了这些后,探索其他内容时就那么迷糊了~ 以下所有的代码为了保持注意力,均会省去类似错误处理这样的旁支代码。

编译准备阶段

此阶段准备好各种配置项,初始化CompilerCompilation对象。

首先整个打包的起点位于webpack.js中的webpack函数,以下是精简的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const webpack = (options, callback) => {
// 校验options有效性
const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
let compiler;
// 结合默认配置项完善options
options = new WebpackOptionsDefaulter().process(options);

compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
// 注册用户自定义插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === 'function') {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}

// 结合options来注册一大坨内置插件
compiler.options = new WebpackOptionsApply().process(options, compiler);

if (callback) {
// watch选项
if (options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {
const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : options.watchOptions || {};
return compiler.watch(watchOptions, callback);
}
compiler.run(callback);
}
return compiler;
};

WebpackOptionsDefaulter继承了OptionsDefaulter类,它的set方法用于设置每一项的默认值,比如
this.set("entry", "./src")就是另entry的默认值为./src.

前半部分代码比较好懂,另外我们暂时不关注watch选项,所以接下来会调用compiler.run。在继续之前先看看WebpackOptionsApply.process

1
2
3
4
5
6
7
8
function process(options, compiler) {
// ... 巨量插件

new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);

// 巨量插件。。。
}

应用插件的代码比较无趣都略过了,比较关键的是EntryOptionPlugin,它监听的事件恰好就是compiler.hooks.entryOption,所以监听函数会立马得到执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const itemToPlugin = (context, item, name) => {
if (Array.isArray(item)) {
return new MultiEntryPlugin(context, item, name);
}
return new SingleEntryPlugin(context, item, name);
};

class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
if (typeof entry === 'string' || Array.isArray(entry)) {
itemToPlugin(context, entry, 'main').apply(compiler);
} else if (typeof entry === 'object') {
for (const name of Object.keys(entry)) {
itemToPlugin(context, entry[name], name).apply(compiler);
}
} else if (typeof entry === 'function') {
new DynamicEntryPlugin(context, entry).apply(compiler);
}
return true;
});
}
}

很明显,针对我们配置的entry选项来应用不同的Plugin,以单入口SingleEntryPlugin为例,另外两个类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SingleEntryPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('SingleEntryPlugin', (compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory);
});

compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
const { entry, name, context } = this;

const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
});
}

static createDependency(entry, name) {
const dep = new SingleEntryDependency(entry);
dep.loc = { name };
return dep;
}
}

监听了两个很重要的事件钩子compilationmake,在我们的流程图里也有这两个钩子,等后续钩子触发我们再看具体的实现。

之后就是调用compiler.run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
run(callback) {
// 。。。
const onCompiled = (err, compilation) => {
// ...
};
this.hooks.beforeRun.callAsync(this, err => {
// 。。。
this.hooks.run.callAsync(this, err => {
// 。。。
this.readRecords(err => {
// 。。。
this.compile(onCompiled);
});
});
});
}

beforeRunrun两个钩子没有什么要特别注意的,最后调用了compile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {

this.hooks.compile.call(params);

const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {

compilation.finish();
// 此时compilation.moudles存放所有生成的modules

compilation.seal(err => {

this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});

});
});
});
}

newCompilationParams会实例化两个工厂类NormalModuleFactoryContextModuleFactory,它们的create方法会创建NormalModuleContextModule,通过这两个Module类我们可以将每个模块结合对应的loader转化为js代码。

1
2
3
4
5
6
7
8
newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory(),
compilationDependencies: new Set()
};
return params;
}

newCompilation用于创建一个Compilation对象,这个对象挂载了各种各样的构建产物,非常核心:

1
2
3
4
5
6
7
8
9
10
11
newCompilation(params) {
const compilation = this.createCompilation();
compilation.fileTimestamps = this.fileTimestamps;
compilation.contextTimestamps = this.contextTimestamps;
compilation.name = this.name;
compilation.records = this.records;
compilation.compilationDependencies = params.compilationDependencies;
this.hooks.thisCompilation.call( compilation, params ); // 最快能够获取到 Compilation 实例的任务点
this.hooks.compilation.call(compilation, params);
return compilation;
}

注意在这里触发了hooks.compilation,还记得之前在SingleEntryPlugin注册了这个钩子吗?

1
2
3
compiler.hooks.compilation.tap('SingleEntryPlugin', (compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory);
});

dependencyFactories是一个Map,用于记录DependencyModuleFactory之间的映射,在后续hooks.make钩子中会用到。

接下来就是hooks.make钩子了,我们的编译准备工作也做完了。

生成moduleschunks

make是代码分析的核心流程,包括创建模块、构建模块的工作,而构建模块步骤又包含了:

  • loader来处理资源
  • 处理后的js进行AST转换并分析语句
  • 语句中发现的依赖关系再处理拆分成dep,即依赖module,形成依赖网
  • chunks 生成,找到 chunk 所需要包含的 modules

SingleEntryPlugin也注册了make钩子:

1
2
3
4
5
6
compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
const { entry, name, context } = this;

const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
});

很简单,看看addEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 // 触发第一批 module 的解析,这些 module 就是 entry 中配置的模块。
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name);

// ...
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
if (module) {
slot.module = module;
} else {
const idx = this._preparedEntrypoints.indexOf(slot);
if (idx >= 0) {
this._preparedEntrypoints.splice(idx, 1);
}
}
this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
}
);
}

基本上就是调用内部方法_addModuleChain,另外就是触发了 2 个钩子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function _addModuleChain(context, dependency, onModule, callback) {
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);

moduleFactory.create(
{
contextInfo: {
issuer: '',
compiler: this.compiler.name,
},
context: context,
dependencies: [dependency],
},
(err, module) => {
let afterFactory;

const addModuleResult = this.addModule(module);
module = addModuleResult.module;

onModule(module);

dependency.module = module;
module.addReason(null, dependency);

const afterBuild = () => {
if (addModuleResult.dependencies) {
this.processModuleDependencies(module, err => {
callback(null, module);
});
} else {
return callback(null, module);
}
};

if (addModuleResult.build) {
this.buildModule(module, false, null, null, err => {
afterBuild();
});
}
},
);
}

this.dependencyFactories.get(Dep)就是我们在compilation钩子中注册的那个,对于SingleEntryDependency我们拿到的是NormalModuleFactory. 看看它的create方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// lib/NormalModuleFactory.js

create(data, callback) {
const dependencies = data.dependencies;
const cacheEntry = dependencyCache.get(dependencies[0]);
if (cacheEntry) return callback(null, cacheEntry);
const context = data.context || this.context;
const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
const request = dependencies[0].request;
const contextInfo = data.contextInfo || {};
this.hooks.beforeResolve.callAsync(
{
contextInfo,
resolveOptions,
context,
request,
dependencies
},
(err, result) => {
if (err) return callback(err);

// Ignored
if (!result) return callback();

const factory = this.hooks.factory.call(null);

// Ignored
if (!factory) return callback();

factory(result, (err, module) => {
if (err) return callback(err);

if (module && this.cachePredicate(module)) {
for (const d of dependencies) {
dependencyCache.set(d, module);
}
}

callback(null, module);
});
}
);
}

如果在beforeResolve钩子中返回false,则后续流程会被跳过,即此模块不会被打包。例如IgnorePlugin处理moment的打包问题就很典型:

测试发现import moment时会将所有locale都打包进了,追查发现在moment源码中有个函数可以执行导入,虽然默认不会执行:

1
2
3
4
5
function loadLocale(name) {
// ...
require('./locale/' + name);
//....
}

webpack打包过程中,在解析完moment后发现有locale的依赖,就会去解析locale。 在IgnorePlugin中打断点发现会尝试解析 ./locale(即result.request的值):

1
2
3
if ('resourceRegExp' in this.options && this.options.resourceRegExp && this.options.resourceRegExp.test(result.request)) {
// ...
}

利用 BundleAnalyzerPlugin 可以很明显发现打包产物包含了所有 locale 文件。

解决办法,添加如下配置:

1
2
3
4
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
});

此时 IgnorePlugin会返回 null,这样我们就跳过了整个locale的打包。 (此插件注册了 NormalModuleFactoryContextModuleFactorybeforeResolve 钩子,locale 的解析是在 ContextModuleFactory 的).

参考资料

以上就是beforeResolve的一个作用,接下来的factory钩子的监听函数中会生成NormalModule实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
this.hooks.factory.tap('NormalModuleFactory', () => (result, callback) => {
// resolver解析 module 需要用到的一些属性,比如需要用到的 loaders, 资源路径 resource 等等,
// 最终将解析完毕的参数传给 NormalModule 构建函数。
let resolver = this.hooks.resolver.call(null);

// Ignored
if (!resolver) return callback();

resolver(result, (err, data) => {
// 此时的data就是module需要的那些属性对象
if (err) return callback(err);

// Ignored
if (!data) return callback();

// direct module
if (typeof data.source === 'function') return callback(null, data);

this.hooks.afterResolve.callAsync(data, (err, result) => {
if (err) return callback(err);

// Ignored
if (!result) return callback();

let createdModule = this.hooks.createModule.call(result);
if (!createdModule) {
if (!result.request) {
return callback(new Error('Empty dependency (no request)'));
}

createdModule = new NormalModule(result);
}

createdModule = this.hooks.module.call(createdModule, result);

return callback(null, createdModule);
});
});
});

传入new NormalModule的参数对象类型示范,这些数据是在resolver监听中生成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
result = {
context,
request,
dependencies,
userRequest,
rawRequest,
loaders,
resource,
matchResource,
resourceResolveData,
settings,
type,
parser,
generator,
resolveOptions,
};

生成了NormalModule后会回到Compilation.prototype._addModuleChain,并在随后调用buildModule方法并传入新创建的NormalModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
buildModule(module, optional, origin, dependencies, thisCallback) {
let callbackList = this._buildingModules.get(module);
if (callbackList) {
callbackList.push(thisCallback);
return;
}
this._buildingModules.set(module, (callbackList = [thisCallback]));

const callback = err => {
this._buildingModules.delete(module);
for (const cb of callbackList) {
cb(err);
}
};

this.hooks.buildModule.call(module);
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
error => {
// module.dependencies表示此module的所有依赖,例如require的其他module
const originalMap = module.dependencies.reduce((map, v, i) => {
map.set(v, i);
return map;
}, new Map());
module.dependencies.sort((a, b) => {
const cmp = compareLocations(a.loc, b.loc);
if (cmp) return cmp;
return originalMap.get(a) - originalMap.get(b);
});

this.hooks.succeedModule.call(module);
return callback();
}
);
}

核心还是NormalModule.prototype.build

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
build(options, compilation, resolver, fs, callback) {
// ...
return this.doBuild(options, compilation, resolver, fs, err => {
const handleParseResult = result => {
this._lastSuccessfulBuildMeta = this.buildMeta;
this._initBuildHash(compilation);
return callback();
};

try {
const result = this.parser.parse(
this._ast || this._source.source(),
{
current: this,
module: this,
compilation: compilation,
options: options
},
(err, result) => {
handleParseResult(result);
}
);
if (result !== undefined) {
// parse is sync
handleParseResult(result);
}
}
});
}

doBuild(options, compilation, resolver, fs, callback) {
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);
// 使用module的loaders处理,得到转化后的js代码,放入_source属性中
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
if (result) {
this.buildInfo.cacheable = result.cacheable;
this.buildInfo.fileDependencies = new Set(result.fileDependencies);
this.buildInfo.contextDependencies = new Set(
result.contextDependencies
);
}

const resourceBuffer = result.resourceBuffer;
const source = result.result[0];
const sourceMap = result.result.length >= 1 ? result.result[1] : null;
const extraInfo = result.result.length >= 2 ? result.result[2] : null;

this._source = this.createSource(
this.binary ? asBuffer(source) : asString(source),
resourceBuffer,
sourceMap
);
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
return callback();
}
);
}

内部使用了runLoaders这个库来转化module如何确定每个Module对应的loaders呢?在初始化NormalModuleFactory时会解析options.rules得到一个ruleSet属性。 ruleSet会去匹配文件类型并结合rules找到需要的loaders

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// lib/NormalModuleFactory.js
constructor(context, resolverFactory, options) {
// ...
this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
// 。。。
const result = this.ruleSet.exec({
resource: resourcePath,
realResource:
matchResource !== undefined
? resource.replace(/\?.*/, "")
: resourcePath,
resourceQuery,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler
});
// 。。。
}
// ...
}

RuleSet的解析过程比较复杂,主要是因为rules配置很灵活,还要兼容一些过时的配置方式,具体过程大家自行了解。

runLoaders在内部主要是读取module内容,再迭代loaders的处理方法,关键的代码有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// loader-runner/lib/LoaderRunner.js

function processResource(options, loaderContext, callback) {
// set loader index to last loader
loaderContext.loaderIndex = loaderContext.loaders.length - 1;

var resourcePath = loaderContext.resourcePath;
if (resourcePath) {
loaderContext.addDependency(resourcePath);
// 读取module内容
options.readResource(resourcePath, function(err, buffer) {
if (err) return callback(err);
options.resourceBuffer = buffer;
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
} else {
iterateNormalLoaders(options, loaderContext, [null], callback);
}
}

function iterateNormalLoaders(options, loaderContext, args, callback) {
if (loaderContext.loaderIndex < 0) return callback(null, args);

//当前的loader
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

// iterate
if (currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}

var fn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
if (!fn) {
return iterateNormalLoaders(options, loaderContext, args, callback);
}

convertArgs(args, currentLoaderObject.raw);

// fn: function ( source, inputSourceMap ) { … }
// 此处执行loader逻辑
runSyncOrAsync(fn, loaderContext, args, function(err) {
if (err) return callback(err);

var args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}

注释中也有写到,一个loader其实就是一个函数:(source: string, inputSourceMap) => string,比如babel-loader

1
2
3
4
5
function (source, inputSourceMap) {
// Make the loader async
const callback = this.async();
loader.call(this, source, inputSourceMap, overrides).then(args => callback(null, ...args), err => callback(err));
};

doBuild方法结束后会拿到module转化后的js代码,并在接下来使用Parser.prototype.parse方法将js转为AST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
parse(source, initialState) {
let ast;
let comments;
if (typeof source === "object" && source !== null) {
ast = source;
comments = source.comments;
} else {
comments = [];
ast = Parser.parse(source, {
sourceType: this.sourceType,
onComment: comments
});
}

const oldScope = this.scope;
const oldState = this.state;
const oldComments = this.comments;
this.scope = {
topLevelScope: true,
inTry: false,
inShorthand: false,
isStrict: false,
definitions: new StackedSetMap(),
renames: new StackedSetMap()
};
const state = (this.state = initialState || {});
this.comments = comments;
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectStrictMode(ast.body);
this.prewalkStatements(ast.body);
this.walkStatements(ast.body);
}
this.scope = oldScope;
this.state = oldState;
this.comments = oldComments;
return state;
}

Parser.parse中是利用acorn这个第三方库来生成AST的,类似的还有esprima

一个Module可能依赖其他Module,这需要逐个解析AST节点来确定,由于依赖的方式有很多种比如requirerequire.ensureimport等,对于每一种依赖都有对应的类例如AMDRequireDependency,依赖的形式如此之多以至于webpack专门建了一个lib/dependencies文件夹。

parser 解析完成之后,module 的解析过程就完成了。每个 module 解析完成之后,都会触发 Compilation 实例对象的任务点 succeedModule,我们可以在这个任务点获取到刚解析完的 module 对象。正如前面所说,module 接下来还要继续递归解析它的依赖模块,最终我们会得到项目所依赖的所有 modules。此时任务点 make 结束。注意require.ensurebuild后被放入了module.blocks而不是module.dependencies

接下来按照流程图我们会调用Compilation对象的finishseal方法。finish很简单就触发了一个钩子,我们的重点放在seal上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
seal(callback) {
this.hooks.seal.call();

this.hooks.beforeChunks.call();
// 根据modules生成chunks。
// webpack 中的 chunk 概念,要不就是配置在 entry 中的模块,要不就是动态引入(比如 require.ensure)的模块。
// chunk 的生成算法:
// 1. webpack 先将 entry 中对应的 module 都生成一个新的 chunk
// 2. 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
// 3. 如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖
// 4. 重复上面的过程,直至得到所有的 chunks
for (const preparedEntrypoint of this._preparedEntrypoints) {
const module = preparedEntrypoint.module;
const name = preparedEntrypoint.name;
const chunk = this.addChunk(name);
const entrypoint = new Entrypoint(name);
entrypoint.setRuntimeChunk(chunk);
entrypoint.addOrigin(null, name, preparedEntrypoint.request);
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint);

GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
GraphHelpers.connectChunkAndModule(chunk, module);

chunk.entryModule = module;
chunk.name = name;

this.assignDepth(module); // 给module设置depth属性
}
// creates the Chunk graph from the Module graph
// 在此时还只有一个入口chunk,处理完后动态引入的module会生成额外chunk
this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice());
this.sortModules(this.modules);
this.hooks.afterChunks.call(this.chunks);

this.hooks.optimize.call();


this.hooks.afterOptimizeModules.call(this.modules);


this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);

this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {


this.hooks.afterOptimizeTree.call(this.chunks, this.modules);


this.hooks.afterOptimizeChunkModules.call(this.chunks, this.modules);

const shouldRecord = this.hooks.shouldRecord.call() !== false;

this.hooks.reviveModules.call(this.modules, this.records);
this.hooks.optimizeModuleOrder.call(this.modules);
this.hooks.advancedOptimizeModuleOrder.call(this.modules);
this.hooks.beforeModuleIds.call(this.modules);
this.hooks.moduleIds.call(this.modules);
this.applyModuleIds();
this.hooks.optimizeModuleIds.call(this.modules);
this.hooks.afterOptimizeModuleIds.call(this.modules);

this.sortItemsWithModuleIds();

this.hooks.reviveChunks.call(this.chunks, this.records);
this.hooks.optimizeChunkOrder.call(this.chunks);
this.hooks.beforeChunkIds.call(this.chunks);
this.applyChunkIds();
this.hooks.optimizeChunkIds.call(this.chunks);
this.hooks.afterOptimizeChunkIds.call(this.chunks);

this.sortItemsWithChunkIds();

if (shouldRecord) {
this.hooks.recordModules.call(this.modules, this.records);
this.hooks.recordChunks.call(this.chunks, this.records);
}

this.hooks.beforeHash.call();
// 生成这次构建的 hash,同时每个chunk也会生成自己的chunkhash
this.createHash();
this.hooks.afterHash.call();

if (shouldRecord) {
this.hooks.recordHash.call(this.records);
}

this.hooks.beforeModuleAssets.call();
this.createModuleAssets();
if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
this.hooks.beforeChunkAssets.call();
// 生成chunk代码文件放入compilation.assets
this.createChunkAssets();
}
this.hooks.additionalChunkAssets.call(this.chunks);
this.summarizeDependencies();
if (shouldRecord) {
this.hooks.record.call(this, this.records);
}

this.hooks.additionalAssets.callAsync(err => {
if (err) {
return callback(err);
}
this.hooks.optimizeChunkAssets.callAsync(this.chunks, err => {
if (err) {
return callback(err);
}
this.hooks.afterOptimizeChunkAssets.call(this.chunks);
this.hooks.optimizeAssets.callAsync(this.assets, err => {
if (err) {
return callback(err);
}
this.hooks.afterOptimizeAssets.call(this.assets);
if (this.hooks.needAdditionalSeal.call()) {
this.unseal();
return this.seal(callback);
}
return this.hooks.afterSeal.callAsync(callback);
});
});
});
});
}

到了seal方法,我们已经处理了所有Module并统一打平放到了compilation.modules中,现在需要根据modules生成chunks,代码中放了一些大致流程的注释,内部的实现还是很复杂的。

moduleIdchunkId作用: 在filename的变换时会用到[id]/[moduleid]chunhHash会用到chunk id; 生成的打包代码会将名称替换为id;

createHash的会生成hash、moduleHash、chunkHash,hash生成算法核心是crypto.createHash + ‘md4’。在随后的createChunkAssets -> TemplatedPathPlugin中替换filenamechunkfileName[hash][chunkhash].

modules生成chunks

没有看完,这里记录掌握的东西。涉及到 3 个核心对象:

  • ChunkGroup: 内部维护了 chunkschildrenparents3 个数组,并添加了一系列方法来维护这 3 个数组。chunks表示这个group下面拥有多少chunk
  • Chunk:内部维护了 groupsmodules 数组。groups表示此 chunk 存在于哪些 chunkgroup 中;modules表示此 chunk 内部含有多少 module
  • Module:内部维护了 chunks 数组。chunks表示此 module 存在于哪些 chunks 当中。

assignDepth方法:从 entry 出发,为每个 module 添加一个 depth 属性

  • entrydepth为 0
  • 依赖的静态模块depth +1
  • 动态模块的depth也是 +1
  • 层级遍历

一些参考文档:

生成了chunks之后,第二阶段就完成了,接下来就是生成打包产物阶段了。

生成打包产物

根据 chunks 生成最终文件,主要有三个步骤:

  • 模板 hash
  • 更新模板渲染 chunk
  • 生成文件

Compilation 在实例化的时候,就会同时实例化三个对象:MainTemplateChunkTemplateModuleTemplate。这三个对象是用来渲染 chunk 对象,得到最终代码的模板。第一个对应了在 entry 配置的入口 chunk 的渲染模板,第二个是动态引入的非入口 chunk 的渲染模板,最后是 chunk 中的 module 的渲染模板。

上面seal方法中调用的createHash就是用于生成模板hash的,hash包含两种:

  • 本次构建的整体hash,用于替换output选项中的[hash],如[name].[hash].js
  • 每个chunk也会生成一个基于内容的hash,用于替换output选项中的[chunkhash],如[name].[chunkhash].js

createHash的主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
createHash() {
const outputOptions = this.outputOptions;
const hashFunction = outputOptions.hashFunction;
const hashDigest = outputOptions.hashDigest;
const hashDigestLength = outputOptions.hashDigestLength;
const hash = createHash(hashFunction);
if (outputOptions.hashSalt) {
hash.update(outputOptions.hashSalt);
}
this.mainTemplate.updateHash(hash);
this.chunkTemplate.updateHash(hash);
for (const key of Object.keys(this.moduleTemplates).sort()) {
this.moduleTemplates[key].updateHash(hash);
}
for (const child of this.children) {
hash.update(child.hash);
}
for (const warning of this.warnings) {
hash.update(`${warning.message}`);
}
for (const error of this.errors) {
hash.update(`${error.message}`);
}
const modules = this.modules;
for (let i = 0; i < modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
module.hash = moduleHash.digest(hashDigest);
module.renderedHash = module.hash.substr(0, hashDigestLength);
}
// clone needed as sort below is inplace mutation
const chunks = this.chunks.slice();
/**
* sort here will bring all "falsy" values to the beginning
* this is needed as the "hasRuntime()" chunks are dependent on the
* hashes of the non-runtime chunks.
*/
chunks.sort((a, b) => {
const aEntry = a.hasRuntime();
const bEntry = b.hasRuntime();
if (aEntry && !bEntry) return 1;
if (!aEntry && bEntry) return -1;
return byId(a, b);
});
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
// 每个chunk也会生成自己的chunkhash
const chunkHash = createHash(hashFunction);
try {
if (outputOptions.hashSalt) {
chunkHash.update(outputOptions.hashSalt);
}
chunk.updateHash(chunkHash);
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
template.updateHashForChunk(
chunkHash,
chunk,
this.moduleTemplates.javascript,
this.dependencyTemplates
);
this.hooks.chunkHash.call(chunk, chunkHash);
chunk.hash = chunkHash.digest(hashDigest);
hash.update(chunk.hash);
chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
this.hooks.contentHash.call(chunk);
} catch (err) {
this.errors.push(new ChunkRenderError(chunk, "", err));
}
}
this.fullHash = hash.digest(hashDigest);
// 本次构建的hash
this.hash = this.fullHash.substr(0, hashDigestLength);
}

hash生成后接下来就会利用createChunkAssets方法生成每个chunk的代码。

首先判断是否为entry来选择Template

1
2
3
// createChunkAssets()

const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate;

然后生成文件名:

1
2
// 根据配置中的 output.filename 来生成文件名称
file = this.getPath(filenameTemplate, fileManifest.pathOptions);

最后根据模板来拼接文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
const manifest = template.getRenderManifest({
chunk,
hash: this.hash,
fullHash: this.fullHash,
outputOptions,
moduleTemplates: this.moduleTemplates,
dependencyTemplates: this.dependencyTemplates,
});

// [{ render(), filenameTemplate, pathOptions, identifier, hash }]
for (const fileManifest of manifest) {
source = fileManifest.render();
}

render方法其实是在chunk内容前后添加各种样板代码,例如MainTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
render(hash, chunk, moduleTemplate, dependencyTemplates) {
// 前置启动代码
const buf = this.renderBootstrap(
hash,
chunk,
moduleTemplate,
dependencyTemplates
);

let source = this.hooks.render.call(
new OriginalSource(
Template.prefix(buf, " \t") + "\n",
"webpack/bootstrap"
),
chunk,
hash,
moduleTemplate,
dependencyTemplates
);
if (chunk.hasEntryModule()) {
source = this.hooks.renderWithEntry.call(source, chunk, hash);
}

chunk.rendered = true;
return new ConcatSource(source, ";");
}

renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates) {
const buf = [];
buf.push(
this.hooks.bootstrap.call(
"",
chunk,
hash,
moduleTemplate,
dependencyTemplates
)
);
buf.push(this.hooks.localVars.call("", chunk, hash));
buf.push("");
buf.push("// The require function");
buf.push(`function ${this.requireFn}(moduleId) {`);
buf.push(Template.indent(this.hooks.require.call("", chunk, hash)));
buf.push("}");
buf.push("");
buf.push(
Template.asString(this.hooks.requireExtensions.call("", chunk, hash))
);
buf.push("");
buf.push(Template.asString(this.hooks.beforeStartup.call("", chunk, hash)));
buf.push(Template.asString(this.hooks.startup.call("", chunk, hash)));
return buf;
}

this.hooks.render.tap(
"MainTemplate",
(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
const source = new ConcatSource();
source.add("/******/ (function(modules) { // webpackBootstrap\n");
source.add(new PrefixSource("/******/", bootstrapSource));
source.add("/******/ })\n");
source.add(
"/************************************************************************/\n"
);
source.add("/******/ (");
source.add(
// 遍历module生成代码
this.hooks.modules.call(
new RawSource(""),
chunk,
hash,
moduleTemplate,
dependencyTemplates
)
);
source.add(")");
return source;
}
);

bootstrap钩子在多个插件中有注册,例如JsonpMainTemplatePlugin中的部分示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
mainTemplate.hooks.bootstrap.tap('JsonpMainTemplatePlugin', (source, chunk, hash) => {
if (needChunkLoadingCode(chunk)) {
const withDefer = needEntryDeferringCode(chunk);
const withPrefetch = needPrefetchingCode(chunk);
return Template.asString([
source,
'',
'// install a JSONP callback for chunk loading',
'function webpackJsonpCallback(data) {',
Template.indent([
'var chunkIds = data[0];',
'var moreModules = data[1];',
withDefer ? 'var executeModules = data[2];' : '',
withPrefetch ? 'var prefetchChunks = data[3] || [];' : '',
'// add "moreModules" to the modules object,',
'// then flag all "chunkIds" as loaded and fire callback',
'var moduleId, chunkId, i = 0, resolves = [];',
'for(;i < chunkIds.length; i++) {',
Template.indent([
'chunkId = chunkIds[i];',
'if(installedChunks[chunkId]) {',
Template.indent('resolves.push(installedChunks[chunkId][0]);'),
'}',
'installedChunks[chunkId] = 0;',
]),
// ...
]);
}
return source;
});

每个chunk的代码生成后就会放到compilation.assets数组中。

生成代码后只剩最后将其输出到文件中了,这一步是在Compiler.prototype.emitAssets函数中:

1
2
3
4
5
6
7
8
9
10
11
const targetPath = this.outputFileSystem.join(outputPath, targetFile);

let content = source.source(); // 待写入的文件内容

if (!Buffer.isBuffer(content)) {
content = Buffer.from(content, 'utf8');
}

source.existsAt = targetPath;
source.emitted = true;
this.outputFileSystem.writeFile(targetPath, content, callback); // 写入文件

以上就是webpack的主体流程了~

参考文章