本文作者:IMWeb llunnn 原文出处:IMWeb社区 未经同意,禁止转载
本篇记录了阅读Compiler.js过程中的一些笔记。(Webpack版本4.41.0)
阅读前需要先对tapable有一定的了解,可参考Tapable github.
这里主要对webpack调用了Compiler.run()到资源输出完毕后经历的过程及其代码进行了一些梳理。
webpack的异步代码基本采用回调函数的形式进行书写,tapable实际上也是注册callback的形式,需要仔细区分各个部分对应的callback。
Compiler类成员变量的类型、含义都比较清晰,也有足够的文档支持,这里不做具体解读了。
Compiler中的方法调用顺序大致如下(以.run为入口):
Compiler.run(callback) 开始执行构建
Compiler.readRecord(callback) 读取之前的构建记录
Compiler.compile(callback) 进行编译
Compiler.newCompilationParams() 创建Compilation的参数
Compiler.newCompilation() 创建新的Compilation
Compiler.emitAssets(compilation, callback) 输出构建资源
Compiler.emitRecords(callback) 输出构建记录
Compiler.run()是整个编译过程启动的入口,在lib/webpack.js中被调用。
// Compiler.run(callback)
run(callback) {
// 如果编译正在进行,抛出错误(一个webpack实例不能同时进行多次编译)
if (this.running) return callback(new ConcurrentCompilationError());
// 定义运行结束的回调
const finalCallback = (err, stats) => {
this.running = false; // 正在运行的标记设为false
if (err) {
// 若有错误,执行failed钩子上的方法
// 我们可以通过compiler.hooks.failed.tap()挂载函数方法
// 其余hooks类似
this.hooks.failed.call(err);
}
if (callback !== undefined) return callback(err, stats);
};
const startTime = Date.now();
// 标记开始运行
this.running = true;
// 调用this.compile传入的回调函数
const onCompiled = (err, compilation) => {
// ...
};
// 执行beforeRun钩子上的方法
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
// 执行run钩子上的方法
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
// 读取之前的records
this.readRecords(err => {
if (err) return finalCallback(err);
// 执行编译
this.compile(onCompiled);
});
});
});
}
readRecords用于读取之前的records的方法,关于records,文档的描述是pieces of data used to store module identifiers across multiple builds(一些数据片段,用于储存多次构建过程中的module的标识)可参考recordsPath。
// Compiler.readRecord(callback)
readRecords(callback) {
// recordsInputPath是webpack配置中指定的读取上一组records的文件路径
if (!this.recordsInputPath) {
this.records = {};
return callback();
}
// inputFileSystem是一个封装过的文件系统,扩展了fs的功能
// 主要是判断一下recordsInputPath的文件是否存在 存在则读取并解析,存到this.records中
// 最后执行callback
this.inputFileSystem.stat(this.recordsInputPath, err => {
// It doesn't exist
// We can ignore this.
if (err) return callback();
this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
if (err) return callback(err);
try {
this.records = parseJson(content.toString("utf-8"));
} catch (e) {
e.message = "Cannot parse records: " + e.message;
return callback(e);
}
return callback();
});
});
}
compile是真正进行编译的过程,创建了一个compilation,并将compilation传给make钩子上的方法,注册在这些钩子上的函数方法会调用compilation上的方法,执行构建。在compilation结束(finish)和封装(seal)完成后,便可以执行传入回调,也就是在Compile.run()中定义的的onCompiled函数。
// Compiler.compile(callback)
compile(callback) {
// 创建了compilation的初始参数
const params = this.newCompilationParams();
// 执行beforeCompile钩子上的方法
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
// 执行compile钩子上的方法
this.hooks.compile.call(params);
// 创建一个新的compilation
const compilation = this.newCompilation(params);
// 执行make钩子上的方法
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
// 若compilation的finish阶段抛出错误,调用callback处理错误
compilation.finish(err => {
if (err) return callback(err);
// 若compilation的seal阶段抛出错误,调用callback处理错误
compilation.seal(err => {
if (err) return callback(err);
// seal完成即编译过程完成
// 执行afterCompile钩子上的方法,传入本次的compilation
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
onCompiled是在Compiler.run中定义的,传给Compiler.compile的回调函数。在compile过程后调用,主要用于输出构建资源。
// Compiler.run(callback) -> onCompiled
const onCompiled = (err, compilation) => {
// finalCallback前面定义的运行结束时回调
if (err) return finalCallback(err);
// 执行shouldEmit钩子上的方法,若返回false则不输出构建资源
if (this.hooks.shouldEmit.call(compilation) === false) {
// stats包含了本次构建过程中的一些数据信息
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
// 执行done钩子上的方法,并传入stats
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
// 调用Compiler.emitAssets输出资源
this.emitAssets(compilation, err => {
if (err) return finalCallback(err);
// 判断资产在emit后是否需要进一步处理
if (compilation.hooks.needAdditionalPass.call()) {
compilation.needAdditionalPass = true;
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
// 执行done钩子上的方法
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
// 执行additionalPass钩子上的方法
this.hooks.additionalPass.callAsync(err => {
if (err) return finalCallback(err);
// 再次compile
this.compile(onCompiled);
});
});
return;
}
// 输出records
this.emitRecords(err => {
if (err) return finalCallback(err);
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
// 执行done钩子上的方法
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
});
});
};
emitAssets负责的是构建资源输出的过程,其中emitFiles是具体输出文件的方法。
// Compiler.emitAssets(compilation, callback)
emitAssets(compilation, callback) {
let outputPath;
// 输出打包结果文件的方法
const emitFiles = err => {
// ...
};
// 执行emit钩子上的方法
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
// 获取资源输出的路径
outputPath = compilation.getPath(this.outputPath);
// 递归创建输出目录,并输出资源
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
}
// Compiler.emitAssets(compilation, callback) -> emitFiles
const emitFiles = err => {
if (err) return callback(err);
// 异步的forEach方法
asyncLib.forEachLimit(
compilation.getAssets(),
15, // 最多同时执行15个异步任务
({ name: file, source }, callback) => {
//
let targetFile = file;
const queryStringIdx = targetFile.indexOf("?");
if (queryStringIdx >= 0) {
targetFile = targetFile.substr(0, queryStringIdx);
}
// 执行写文件操作
const writeOut = err => {
// ...
};
// 若目标文件路径包含/或\,先创建文件夹再写入
if (targetFile.match(/\/|\\/)) {
const dir = path.dirname(targetFile);
this.outputFileSystem.mkdirp(
this.outputFileSystem.join(outputPath, dir),
writeOut
);
} else {
writeOut();
}
},
// 遍历完成的回调函数
err => {
if (err) return callback(err);
// 执行afterEmit钩子上的方法
this.hooks.afterEmit.callAsync(compilation, err => {
if (err) return callback(err);
// 构建资源输出完成执行回调
return callback();
});
}
);
};
writeOut函数进行具体的写文件操作。
其中涉及到的两个内部Map:
_assetEmittingSourceCache用于记录资源在不同目标路径被写入的次数。
_assetEmittingWrittenFiles用于标记目标路径已经被写入的次数,key是targetPath。每次targetPath被文件写入,其对应的value会自增。
/** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */
this._assetEmittingSourceCache = new WeakMap();
/** @private @type {Map<string, number>} */
this._assetEmittingWrittenFiles = new Map();
关于futureEmitAssets配置项可参考output.futureEmitAssets,这里对基于垃圾回收做的内存优化(SizeOnlySource部分)还是比较有意思的。
// Compiler.emitAssets(compilation, callback) -> emitFiles -> writeOut
const writeOut = err => {
if (err) return callback(err);
// 解析出真实的目标路径
const targetPath = this.outputFileSystem.join(
outputPath,
targetFile
);
// TODO webpack 5 remove futureEmitAssets option and make it on by default
if (this.options.output.futureEmitAssets) {
// check if the target file has already been written by this Compiler
// 检查目标文件是否已经被这个Compiler写入过
// targetFileGeneration是targetFile被写入的次数
const targetFileGeneration = this._assetEmittingWrittenFiles.get(
targetPath
);
// create an cache entry for this Source if not already existing
// 若cacheEntry不存在,则为当前source创建一个
let cacheEntry = this._assetEmittingSourceCache.get(source);
if (cacheEntry === undefined) {
cacheEntry = {
sizeOnlySource: undefined,
writtenTo: new Map() // 存储资源被写入的目标路径及其次数,对应this._assetEmittingWrittenFiles的格式
};
this._assetEmittingSourceCache.set(source, cacheEntry);
}
// if the target file has already been written
// 如果目标文件已经被写入过
if (targetFileGeneration !== undefined) {
// check if the Source has been written to this target file
// 检查source是否被写到了目标文件路径
const writtenGeneration = cacheEntry.writtenTo.get(targetPath);
if (writtenGeneration === targetFileGeneration) {
// if yes, we skip writing the file
// as it's already there
// (we assume one doesn't remove files while the Compiler is running)
// 如果等式成立,我们跳过写入当前文件,因为它已经被写入过
// (我们假设Compiler在running过程中文件不会被删除)
compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
size: cacheEntry.sizeOnlySource.size()
});
return callback();
}
}
// TODO webpack 5: if info.immutable check if file already exists in output
// skip emitting if it's already there
// get the binary (Buffer) content from the Source
// 获取source的二进制内容content
/** @type {Buffer} */
let content;
if (typeof source.buffer === "function") {
content = source.buffer();
} else {
const bufferOrString = source.source();
if (Buffer.isBuffer(bufferOrString)) {
content = bufferOrString;
} else {
content = Buffer.from(bufferOrString, "utf8");
}
}
// Create a replacement resource which only allows to ask for size
// This allows to GC all memory allocated by the Source
// (expect when the Source is stored in any other cache)
// 创建一个source的代替资源,其只有一个size方法返回size属性(sizeOnlySource)
// 这步操作是为了让垃圾回收机制能回收由source创建的内存资源
//
// 这里是设置了output.futureEmitAssets = true时,assets的内存资源会被释放的原因
cacheEntry.sizeOnlySource = new SizeOnlySource(content.length);
compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
size: content.length
});
// Write the file to output file system
// 将content写到目标路径targetPath
this.outputFileSystem.writeFile(targetPath, content, err => {
if (err) return callback(err);
// information marker that the asset has been emitted
compilation.emittedAssets.add(file);
// cache the information that the Source has been written to that location
// 缓存source已经被写入目标路径,写入次数自增
const newGeneration =
targetFileGeneration === undefined
? 1
: targetFileGeneration + 1;
// 将这个自增的值写入cacheEntry.writtenTo和this._assetEmittingWrittenFiles两个Map中
cacheEntry.writtenTo.set(targetPath, newGeneration);
this._assetEmittingWrittenFiles.set(targetPath, newGeneration);
// 执行assetEmitted钩子上的方法
this.hooks.assetEmitted.callAsync(file, content, callback);
});
} else { // webpack4的默认配置output.futureEmitAssets = false
// 若资源已存在在目标路径 则跳过
if (source.existsAt === targetPath) {
source.emitted = false;
return callback();
}
// 获取资源内容
let content = source.source();
if (!Buffer.isBuffer(content)) {
content = Buffer.from(content, "utf8");
}
// 写入目标路径并标记
source.existsAt = targetPath;
source.emitted = true;
this.outputFileSystem.writeFile(targetPath, content, err => {
if (err) return callback(err);
// 执行assetEmitted钩子上的方法
this.hooks.assetEmitted.callAsync(file, content, callback);
});
}
};
至此,Compiler完成了启动构建到资源输出到过程。