再读 Webpack 源码,各个击破(三):Module 及其处理器 Loader

前言

Webpack 是一个打包工具,实际开发中我们可能采用模块化、组件化进行开发,这些单个的文件(模块,Module)通过 rule 的配置使用不同的 Loader 进行处理,并从入口文件经过依赖解析最终输出一个或多个打包文件(模块代码的集合)。前面已经介绍了 Entry 入口文件的处理,本篇将对 Module 的处理部分进行源码分析。


1、前情提要

在第一篇中我们介绍了Compiler 到 Compilation 执行的过程,通过 entry 入口文件再到 Compilation.buildModule 进行模块处理,buildModule 才真正到了模块的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
node_modules\webpack\lib\Compilation.js
class Compilation extends Tapable {
buildModule(module, optional, origin, dependencies, thisCallback) {
……
// 在模块构建开始之前触发,可以用来修改模块。
this.hooks.buildModule.call(module);
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
error => {
……
// 模块构建成功时执行。这里会解析出依赖模块,可以进行一些处理,比如依赖模块移除
this.hooks.succeedModule.call(module);
return callback();
}
);
}
}

在正式进入模块的内容之前,我们先对 NormalModuleFactory 进行介绍。在第一篇中我们在看到它被创建的一个过程,webpack 通过 NormalModuleFactory 来创建各类模块。同时,它也是 Tapable 的子类。因此,该类也有很多的 Hooks 供开发者使用。本篇不会对其做过多的源码解读,主要介绍下关键部分(这里我们会配合第一篇中 compilation 的 _addModuleChain 代码进行介绍):

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
node_modules\webpack\lib\Compilation.js
_addModuleChain(context, dependency, onModule, callback) {
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
// dependencyFactories 里有很多类型的工厂对象,通过依赖的具体类型拿到工厂对象,暂时忽略下
const moduleFactory = this.dependencyFactories.get(Dep);
this.semaphore.acquire(() => {
// 通过模块工厂对象创建模块,工厂的 create 方法,我们先放一下,在后面说。
moduleFactory.create({},//省略)
}
}
node_modules\webpack\lib\NormalModuleFactory.js
class NormalModuleFactory extends Tapable {
constructor(context, resolverFactory, options) {
super();
// 对配置在module中的rules进行解析
this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
// 调用resolver,得到 loaders 等数据
let resolver = this.hooks.resolver.call(null);
resolver(result, (err, data) => {
this.hooks.afterResolve.callAsync(data, (err, result) => {
let createdModule = this.hooks.createModule.call(result);
if (!createdModule) {
// 创建模块对象
createdModule = new NormalModule(result);
}
createdModule = this.hooks.module.call(createdModule, result);
return callback(null, createdModule);
});
});
});
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
asyncLib.parallel([],
(err, results) => {
asyncLib.parallel([],
(err, results) => {
process.nextTick(() => {
// resolver 最终是为了返回下面这部分信息
callback(null, {
context: context,
request: loaders
.map(loaderToIdent)
.concat([resource])
.join("!"),
dependencies: data.dependencies,
userRequest,
rawRequest: request,
loaders,
resource,
});
});
}
);
}
);
});
}
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({},
(err, result) => {
const factory = this.hooks.factory.call(null);
factory(result, (err, module) => {
callback(null, module);
});
}
);
}
}

Compilation 的 _addModuleChain 会调用 NormalModuleFactory 的 create 方法拿到一个 NormalModule 模块实例,中间主要是通过 NormalModuleFactory.hooks.resolver 拿到 loaders 等信息返回给 NormalModuleFactory.hooks.factory,然后去创建一个模块实例并通过回调返回。loaders 是在 NormalModuleFactory 的构造方法中通过 RuleSet 解析获得的。ok,那么通过这个过程,一个真正的模块(Module)实例就得到了,而且模块中包含它的 request 路径、loaders 处理器、context上下文(其实就是引用该模块的模块所在目录)、依赖模块等信息。这个模块可以通过路径信息进去读取,并被 loaders 进行处理。

2、Module

在前面的代码中我们已经看到,Compilation 的 buildModule 方法最红是调用了 Module 的 build方法。Module 类是一个基类,它的 build 方法并未实现,这个方法是在子类中被实现的。在上面对 NormalModuleFactory 的介绍中,我们看到它生成了一个 NormalModule 实例,我们将对这个类的 build 方法进行介绍,进入 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
node_modules\webpack\lib\Module.js
Module.prototype.build = null;
node_modules\webpack\lib\NormalModule.js
class NormalModule extends Module {
build(options, compilation, resolver, fs, callback) {
……
return this.doBuild(options, compilation, resolver, fs, err => {
this._cachedSources.clear();
……
const handleParseResult = result => {
this._lastSuccessfulBuildMeta = this.buildMeta;
this._initBuildHash(compilation);
return callback();
};
try {
const result = this.parser.parse(
this._ast || this._source.source(), {},
(err, result) => {
if (err) {
handleParseError(err);
} else {
handleParseResult(result);
}
}
);
if (result !== undefined) {
// parse is sync
handleParseResult(result);
}
} catch (e) {
handleParseError(e);
}
});
}
doBuild(options, compilation, resolver, fs, callback) {
const loaderContext = this.createLoaderContext();
//
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs) //读取模块文件内容
},
(err, result) => {
……
return callback();
}
);
}
}

从源码上,build 方法是执行了 doBuild 方法。doBuild 最终走到了 runLoaders,这个方法由 loader-runner 库提供,这是一个独立的库。它可以用来开发调试你的自定义 Loader,我们在接下来的小节中去介绍。在这里 NormalModule 并没有过多的处理工作,基本就是交给 loaders 去处理模块内容。loaders 处理完之后使用了 parser 对象进行处理,最后是 _initBuildHash 去创建 hash。

3、loader-runner 库

笔者使用的 loader-runner 库版本如下:

1
2.4.0

现在,我们来看 runLoaders 方法的源码到底做了哪些事情:

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
node_modules\loader-runner\lib\LoaderRunner.js
exports.runLoaders = function runLoaders(options, callback) {
// read options
var resource = options.resource || "";
var loaders = options.loaders || []; // 读取 Loaders
var loaderContext = options.context || {};
var readResource = options.readResource || readFile;
……
var processOptions = {
resourceBuffer: null,
readResource: readResource
};
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
callback(null, {
result: result,
resourceBuffer: processOptions.resourceBuffer,
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies
});
});
};

我们看 runLoaders 方法对于传入的 options 进行了拆解和重新组装并调用了 iteratePitchingLoaders 方法。在 Webpack 中执行 loader 的过程分为两个阶段:pitch 和 evaluating。这里可以理解为事件里的 capturing 阶段,还没到执行。在 pitching 过程中你可以拦截执行。现在,我们来看 iteratePitchingLoaders 相关部分代码:

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
function iteratePitchingLoaders(options, loaderContext, callback) {
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// pitch 过程,currentLoaderObject 不断走向最后一个loader 的过程
if(currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
// 加载 loader 模块
loadLoader(currentLoaderObject, function(err) {
var fn = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
// 执行
runSyncOrAsync(
fn,
loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) {
var args = Array.prototype.slice.call(arguments, 1);
if(args.length > 0) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
});
}
function processResource(options, loaderContext, callback) {
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
var resourcePath = loaderContext.resourcePath;
if(resourcePath) {
loaderContext.addDependency(resourcePath);
options.readResource(resourcePath, function(err, buffer) {
if(err) return callback(err);
options.resourceBuffer = buffer;
// 读取模块内容,交给loader去处理
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
} else {
iterateNormalLoaders(options, loaderContext, [null], callback);
}
}
function runSyncOrAsync(fn, context, args, callback) {
context.async = function async() {//loader 异步处理拿到的回调
if(isDone) {
if(reportedError) return; // ignore
throw new Error("async(): The callback was already called.");
}
isSync = false;
return innerCallback;
};
var innerCallback = context.callback = function() {
try {
执行下一个loader
callback.apply(null, arguments);
} catch(e) {
}
};
try {
var result = (function LOADER_EXECUTION() {
// fn 就是最终具体 loader 的执行函数
return fn.apply(context, args);
}());
if(isSync) {
isDone = true;
if(result === undefined)
return callback();
if(result && typeof result === "object" && typeof result.then === "function") {
return result.then(function(r) {
callback(null, r);
}, callback);
}
// 执行下一个loader
return callback(null, result);
}
} catch(e) {
callback(e);
}
}
function iterateNormalLoaders(options, loaderContext, args, callback) {
// 全部执行完了,执行callback,也就是 NormalModule.doBuild这里调用 runLoaders 给到的回调代码
if(loaderContext.loaderIndex < 0)
return callback(null, args);
runSyncOrAsync(fn, loaderContext, args, function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}

上面的源码中出现了几个方法:

  • iteratePitchingLoaders,按照配置从头找到尾,然后从最后的 loader 开始执行
  • iterateNormalLoaders,就是 iteratePitchingLoaders 的逆过程,依次执行 loader 的过程。这也可以解释为什么执行顺序和配置顺序是相反的。并且该方法里会判断退出时机
  • processResource,决定 loader 的输入是读取模块内容还是上一个 loader 的输出
  • runSyncOrAsync,真正调用 loader 函数的地方,并且给 loader 函数的 this 挂载 async 方法实现异步

4、再续前缘

ok,分析到这里。我们看到 webpack 是如何从 entry 到 module,最后执行 loader 处理 module 的过程。经过 loader 处理之后,最终一路 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
class Compilation extends Tapable {
addEntry(context, entry, name, callback) {
if(callback){}
this.hooks.addEntry.call(entry, name);
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
if (err) {
this.hooks.failedEntry.call(entry, name, err);
return callback(err);
}
// 执行 hooks.succeedEntry
this.hooks.succeedEntry.call(entry, name, module);
//执行回调
return callback(null, module);
}
);
}
}

addEntry 最后执行了 callback,最终回调到了哪里? 还记得我们之前讲到的下面这段代码吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
node_modules\webpack\lib\MultiEntryPlugin.js
class MultiEntryPlugin {
apply(compiler) {
compiler.hooks.make.tapAsync(
"MultiEntryPlugin",
(compilation, callback) => {
const { context, entries, name } = this;
const dep = MultiEntryPlugin.createDependency(entries, name);
compilation.addEntry(context, dep, name, callback);
}
);
}
}

至此,结合前面 Tapable 的内容,这里最终 callback 回到了 Compilation 的 finish。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
node_modules\webpack\lib\Compiler.js
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});

我们来看一下 Compilation 的 finish :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
finish(callback) {
const modules = this.modules;
// 调用 hooks.finishModules,entry 的全部模块执行完会执行它的 callback,
// 一次打包有多个 entry 就会有多次 finishModules。
this.hooks.finishModules.callAsync(modules, err => {
if (err) return callback(err);
for (let index = 0; index < modules.length; index++) {
const module = modules[index];
this.reportDependencyErrorsAndWarnings(module, [module]);
}
callback();
});
}

那么 callback 之后来到了 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
seal(callback) {
this.hooks.seal.call();
while (
this.hooks.optimizeDependenciesBasic.call(this.modules) ||
this.hooks.optimizeDependencies.call(this.modules) ||
this.hooks.optimizeDependenciesAdvanced.call(this.modules)
) {
/* empty */
}
this.hooks.afterOptimizeDependencies.call(this.modules);
this.hooks.beforeChunks.call();
buildChunkGraph(
this,
/** @type {Entrypoint[]} */ (this.chunkGroups.slice())
);
this.sortModules(this.modules);
this.hooks.afterChunks.call(this.chunks);
this.hooks.optimize.call();
while (
this.hooks.optimizeModulesBasic.call(this.modules) ||
this.hooks.optimizeModules.call(this.modules) ||
this.hooks.optimizeModulesAdvanced.call(this.modules)
) {
/* empty */
}
this.hooks.afterOptimizeModules.call(this.modules);
while (
this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||
this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
) {
/* empty */
}
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);
while (
this.hooks.optimizeChunkModulesBasic.call(this.chunks, this.modules) ||
this.hooks.optimizeChunkModules.call(this.chunks, this.modules) ||
this.hooks.optimizeChunkModulesAdvanced.call(this.chunks, this.modules)
) {
/* empty */
}
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();
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();
this.createChunkAssets();
}
this.hooks.additionalChunkAssets.call(this.chunks);
this.summarizeDependencies();
if (shouldRecord) {
this.hooks.record.call(this, this.records);
}
this.hooks.additionalAssets.callAsync(err => {
this.hooks.optimizeChunkAssets.callAsync(this.chunks, err => {
this.hooks.afterOptimizeChunkAssets.call(this.chunks);
this.hooks.optimizeAssets.callAsync(this.assets, 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 方法这段源码非常多,篇幅有限,我们并不会展开来讲。但是我们看到大部分 hooks 和方法的关键词 optimize、chunk 还有 hash。在执行 seal 方法之前,这里的 module 都是经过 loader 处理过的代码,还需要进行优化操作,比如压缩。最终在 this.hooks.afterSeal.callAsync 执行后回到 compiler 的 hooks.shouldEmit.call 以及 hooks.done.callAsync。到 done 这个 hooks(done 是只会执行一次的),基本上就是完成了打包文件的输出工作了,这也意味着一次完整的打包流程就完成了。

PS:seal 里面有很多的 hooks 执行了 call 或 callAsync 方法。我们在第一章中提到过 node_modules\webpack\lib\WebpackOptionsApply.js 这个类里用到了很多的插件,比如 SplitChunksPlugin、FlagDependencyUsagePlugin,它们添加的钩子都在这里执行。关于插件,我们在下一章去分析。