webpack源码阅读之Compiler

本篇记录了阅读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(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);

});

});

});

}

Compiler.readRecord(callback)

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();

});

});

}

Compiler.compile(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);

});

});

});

});

});

}

Compiler.run(callback) -> onCompiled

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);

});

});

});

};

Compiler.emitAssets(compilation, callback)

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();

});

}

);

};

Compiler.emitAssets(compilation, callback) -> emitFiles -> writeOut

writeOut函数进行具体的写文件操作。

其中涉及到的两个内部Map:

_assetEmittingSourceCache用于记录资源在不同目标路径被写入的次数。

_assetEmittingWrittenFiles用于标记目标路径已经被写入的次数,key是targetPath。每次targetPath被文件写入,其对应的value会自增。

/** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */

this._assetEmittingSourceCache = newWeakMap();

/** @private @type {Map<string, number>} */

this._assetEmittingWrittenFiles = newMap();

关于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: newMap() // 存储资源被写入的目标路径及其次数,对应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完成了启动构建到资源输出到过程。

以上是 webpack源码阅读之Compiler 的全部内容, 来源链接: www.h5w3.com/115522.html

回到顶部