buildModule
_buildModule
_buildModule最终调用的是module.build方法,这里以NormalModule为例,找到webpack/lib/NormalModule.js文件中的build方法:
// 源码
this._source = null;
// 源码的 ast
this._ast = null;
// 打包信息
this.buildInfo = {
cacheable: false,
parsed: true,
// 依赖文件
fileDependencies: undefined,
contextDependencies: undefined,
// 未找到的依赖
missingDependencies: undefined,
// loader 依赖
buildDependencies: undefined,
// 变量依赖
valueDependencies: undefined,
// hash 值
hash: undefined,
assets: undefined,
// 文件信息
assetsInfo: undefined
};首先会定义一些参数,最后调用doBuild方法开始正式打包。
loaderContext
doBuild方法首先会创建一个loader上下文loaderContext:
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);loaderContext的用于在执行loader时传入作为上下文,这样我们在编写loader的时候就可以通过this访问这个上下文里的一些方法属性了。
runLoaders
doBuild第二个任务是执行runLoaders函数,精简后的代码如下:
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
processResource: (loaderContext, resource, callback) => {
loaderContext.addDependency(resource);
fs.readFile(resource, callback);
}
},
(err, result) => {
}
);该过程会通过readFile读取文件源码,并执行匹配到的loader对读取到的代码进行处理。runLoaders函数是在node_modules/loader-runner/lib/LoaderRunner.js文件中定义:
exports.runLoaders = function runLoaders(options, callback) {
var loaders = options.loaders || [];
loaders = loaders.map(createLoaderObject);
// loaderContext 的一些属性添加...
iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {});
}首先会在loaderContext上添加一系列的属性方法,其中也包括loaders,通过createLoaderObject方法将路径形式的loader装换为对象形式。最后调用iteratePitchingLoaders执行loaders。
iteratePitchingLoaders
iteratePitchingLoaders代码如下:
function iteratePitchingLoaders(options, loaderContext, callback) {
// 1. loaders 执行完了开始处理资源
if (loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
// 2. 获取 loader
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 3. 判断 loader 是否 pitch,如果 pitch 执行过,那么执行下一个 loader 的 pitch
if (currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
// 4. 加载 loader
loadLoader(currentLoaderObject, function (err) {});
}loader的执行过程与事件冒泡有点类似,它包括pitch阶段,代码读取,loader执行三个阶段(具体可以参考这篇文章)。pitch阶段会通过iteratePitchingLoaders方法遍历loaders,执行loader的pitch函数。待执行完成后会读取文件代码,然后通过iterateNormalLoaders方法反向遍历loaders执行loader。
iteratePitchingLoaders中通过pitchExecuted属性标识loader是否被pitch。如果为true,那么会执行下一个loader的pitch函数。否则的话会通过loadLoader函数加载loader。
loadLoader
loader-runner/lib/loadLoader.js文件中loadLoader代码精简如下:
module.exports = function loadLoader(loader, callback) {
var module = require(loader.path);
return handleResult(loader, module, callback);
};
function handleResult(loader, module, callback) {
loader.normal = typeof module === "function" ? module : module.default;
loader.pitch = module.pitch;
loader.raw = module.raw;
callback();
}加载loader模块后将加载的loader模块、pitch函数等赋值到loader对象上。最后执行loadLoader回调,这里讨论的是iteratePitchingLoaders中loadLoader的回调:
var fn = currentLoaderObject.pitch;
// 标识 pitch
currentLoaderObject.pitchExecuted = true;
// 如果 pitch 函数不存在,那么开始执行下一个 loader 的 pitch
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);
var hasArg = args.some(function (value) {
return value !== undefined;
});
if (hasArg) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);如果pitch函数存在,那么会通过runSyncOrAsync执行pitch函数:
function runSyncOrAsync(fn, context, args, callback) {
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args);
}());
if (isSync) {
if (result === undefined)
return callback();
if (result && typeof result === "object" && typeof result.then === "function") {
return result.then(function (r) {
callback(null, r);
}, callback);
}
return callback(null, result);
}
}最后会将结果result传入给回调函数callback。再来看下callback:
var args = Array.prototype.slice.call(arguments, 1);
var hasArg = args.some(function (value) {
return value !== undefined;
});首先会用hasArg判断pitch函数返回的结果是否存在:
if (hasArg) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
iteratePitchingLoaders(options, loaderContext, callback);
}如果存在,那么就会开始调用iterateNormalLoaders函数,终止了pitch过程,此时没有processResource过程。否则会调用iteratePitchingLoaders继续下一个loader的pitch。
processResource
如果是pitch函数全部正常执行且没有返回值,那么最终会执行processResource方法,也就是runLoaders调用的参数:
processResource: (loaderContext, resource, callback) => {
loaderContext.addDependency(resource);
fs.readFile(resource, callback);
}首先会将文件路径作为loaderContext的dependency添加。其次会读取资源,最终执行回调中的iterateNormalLoaders:
options.resourceBuffer = buffer;
iterateNormalLoaders(options, loaderContext, [buffer], callback);iterateNormalLoaders
iterateNormalLoaders的执行过程与iteratePitchingLoaders类似。唯一需要注意的是它传入的参数是文件资源读取的内容,每一次loader后都会将结果返回作为上一个loader的参数传入。
iterateNormalLoaders(options, loaderContext, [buffer], callback);pitch打断过程实际上相当于提前返回了文件资源内容。
等所有的loader执行完成后,开始执行runLoaders的回调:
(err, result) => {
this.buildInfo.fileDependencies.addAll(result.fileDependencies);
this.buildInfo.contextDependencies.addAll(result.contextDependencies);
this.buildInfo.missingDependencies.addAll(result.missingDependencies);
for (const loader of this.loaders) {
this.buildInfo.buildDependencies.add(loader.loader);
}
this.buildInfo.cacheable = this.buildInfo.cacheable && result.cacheable;
processResult(err, result.result);
}返回的结果result包含了文件加载后并通过了loader处理的代码。并且用到的loader都会添加到buildInfo.buildDependencies当中。最后再通过processResult处理源码。
processResult
processResult主要作用是将源码内容封装成RawSource对象。
this._source = this.createSource(
options.context,
this.binary ? asBuffer(source) : asString(source),
sourceMap,
compilation.compiler.root
);
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
callback()随后会执行callback,也就是_doBuild方法的回调函数,其核心代码如下:
// 1. 获取源码
const source = this._source.source();
// 2. 解析源码
result = this.parser.parse(this._ast || source, {
source,
current: this,
module: this,
compilation: compilation,
options: options
});
} catch (e) {
handleParseError(e);
return;
}
// 3. 处理解析后的源码
handleParseResult(result);parse
解析代码的parse方法在webpack/lib/javascript/JavascriptParser.js文件中定义:
parse(source, state) {
let ast;
// 1. 解析成 ast 树
ast = JavascriptParser._parse(source, {
sourceType: this.sourceType,
onComment: comments,
onInsertedSemicolon: pos => semicolons.add(pos)
});
// ...
// 2. 转换
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectMode(ast.body);
this.preWalkStatements(ast.body);
this.prevStatement = undefined;
this.blockPreWalkStatements(ast.body);
this.prevStatement = undefined;
this.walkStatements(ast.body);
}
return state;
}其中JavascriptParser._parse方法使用的是acorn库,解析过程可以在这里进行在线调试。
hooks.program
hooks.program的调用触发了四个回调函数。
CompatibilityPlugin
处理第一行开头为#!开头的注释,创建一个ConstDependency添加到module.presentationalDependencies中:
const dep = new ConstDependency("//", 0);
dep.loc = c.loc;
parser.state.module.addPresentationalDependency(dep);HarmonyDetectionParserPlugin
处理import和export语法。如果存在import/export语法,那么会创建一个HarmonyCompatibilityDependency添加到module.presentationalDependencies中:
statement.type === "ImportDeclaration" ||
statement.type === "ExportDefaultDeclaration" ||
statement.type === "ExportNamedDeclaration" ||
statement.type === "ExportAllDeclaration"
const compatDep = new HarmonyCompatibilityDependency();
module.addPresentationalDependency(compatDep);UseStrictPlugin
如果第一句以'use strict'开头,会创建一个空字符串的ConstDependency。因为后续webpack会自动添加use strict,所以这里需要移除:
const dep = new ConstDependency("", firstNode.range);
parser.state.module.addPresentationalDependency(dep);DefinePlugin
定义全局变量:
buildInfo.valueDependencies.set(VALUE_DEP_MAIN, mainValue);walkStatements
walkStatements的过程是将ast树进行解析转换,这里主要以import/export为例。
ImportDeclaration
ImportDeclaration语句(如import A from './a.js')会触发hooks.import,执行HarmonyImportDependencyParserPlugin的回调:
parser.hooks.import.tap(
"HarmonyImportDependencyParserPlugin",
(statement, source) => {
// 1. import 的序号
parser.state.lastHarmonyImportOrder =
(parser.state.lastHarmonyImportOrder || 0) + 1;
const clearDep = new ConstDependency(
parser.isAsiPosition(statement.range[0]) ? ";" : "",
statement.range
);
clearDep.loc = statement.loc;
// 2. 替换成一个 clearDep
parser.state.module.addPresentationalDependency(clearDep);
parser.unsetAsiPosition(statement.range[1]);
const assertions = getAssertions(statement);
// 3. 添加一个 dependency
const sideEffectDep = new HarmonyImportSideEffectDependency(
source,
parser.state.lastHarmonyImportOrder,
assertions
);
sideEffectDep.loc = statement.loc;
parser.state.module.addDependency(sideEffectDep);
return true;
}
)添加完dependency后,开始定义变量:
for (const specifier of statement.specifiers) {
const name = specifier.local.name;
switch (specifier.type) {
case "ImportDefaultSpecifier":
if (
!this.hooks.importSpecifier.call(statement, source, "default", name)
) {
this.defineVariable(name);
}
break;
case "ImportSpecifier":
if (
!this.hooks.importSpecifier.call(
statement,
source,
specifier.imported.name,
name
)
) {
this.defineVariable(name);
}
break;
case "ImportNamespaceSpecifier":
if (!this.hooks.importSpecifier.call(statement, source, null, name)) {
this.defineVariable(name);
}
break;
default:
this.defineVariable(name);
}
}ExportNamedDeclaration
如export const a = 0,会触发hooks.export:
parser.hooks.export.tap(
"HarmonyExportDependencyParserPlugin",
statement => {
const dep = new HarmonyExportHeaderDependency(
statement.declaration && statement.declaration.range,
statement.range
);
dep.loc = Object.create(statement.loc);
dep.loc.index = -1;
// 添加 dependency
parser.state.module.addPresentationalDependency(dep);
return true;
}
);hooks.finish
解析源码结束后调用hooks.finish。该hook除清除一些遍历引用之外,还会将解析过程中定义的变量添加到topLevelDeclarations中:
parser.hooks.finish.tap("JavascriptMetaInfoPlugin", () => {
let topLevelDeclarations =
parser.state.module.buildInfo.topLevelDeclarations;
if (topLevelDeclarations === undefined) {
topLevelDeclarations =
parser.state.module.buildInfo.topLevelDeclarations = new Set();
}
for (const name of parser.scope.definitions.asSet()) {
const freeInfo = parser.getFreeInfoFromVariable(name);
if (freeInfo === undefined) {
// 添加变量名称
topLevelDeclarations.add(name);
}
}
});handleParseResult
handleParseResult会为module创建hash:
_initBuildHash(compilation) {
const hash = createHash(compilation.outputOptions.hashFunction);
if (this._source) {
hash.update("source");
this._source.updateHash(hash);
}
hash.update("meta");
hash.update(JSON.stringify(this.buildMeta));
this.buildInfo.hash = /** @type {string} */ (hash.digest("hex"));
}随后会将依赖生成snapshot,记录在buildInfo上:
compilation.fileSystemInfo.createSnapshot(
startTime,
this.buildInfo.fileDependencies,
this.buildInfo.contextDependencies,
this.buildInfo.missingDependencies,
snapshotOptions,
(err, snapshot) => {
this.buildInfo.fileDependencies = undefined;
this.buildInfo.contextDependencies = undefined;
this.buildInfo.missingDependencies = undefined;
this.buildInfo.snapshot = snapshot;
return callback();
}
);最终执行callback,回到了compilation.buildModule的回调函数当中:
this.buildModule(module, err => {
this.processModuleDependencies(module, err => {
callback(null, module);
});
});processModuleDependencies
processModuleDependencies对应于_processModuleDependencies方法:
if (block.dependencies) {
currentBlock = block;
let i = 0;
for (const dep of block.dependencies) processDependency(dep, i++);
}该方法遍历打包后的module的dependencies,执行processDependency:
const processDependency = (dep, index) => {
this.moduleGraph.setParents(dep, currentBlock, module, index);
// ...
processDependencyForResolving(dep);
};
const processDependencyForResolving = dep => {
const resourceIdent = dep.getResourceIdentifier();
if (resourceIdent !== undefined && resourceIdent !== null) {
const category = dep.category;
const factory = this.dependencyFactories.get(constructor);
sortedDependencies.push({
factory: factoryCacheKey2,
dependencies: list,
originModule: module
});
}
};首先通过moduleGraph.setParents建立dependency和当前module的联系。其次根据dependency获取创建module的工厂函数,并添加到sortedDependencies中,最后会执行onDependenciesSorted方法:
for (const item of sortedDependencies) {
this.handleModuleCreation(item, err => {
// ...
});
}对于每个依赖的dependency,递归调用handleModuleCreation创建module。至此,整个打包过程就完成了。
总结
buildModule阶段主要任务是将生成的module进行打包,这个过程包含:
首先,创建loaderContext,形成loader的执行上下文。通过runLoaders执行该文件匹配到的loaders。
loader的执行过程又包含pitch阶段,源码读取阶段,loader执行阶段。
pitch阶段会调用loadLoader加载loader模块,然后调用pitch函数。可以通过pitch函数提前返回源码内容中断后续loader的调用。源码读取阶段会直接读取对应文件的源码内容。
loader执行阶段会依次从后向前执行loader函数,每次都会将执行结果作为下一个loader函数的参数传入。
其次,loaders执行完成后拿到加载后的源码内容,通过acorn库对源码内容进行解析,形成ast。
然后通过hooks.program和walkStatements对ast树进行分析,如变量定义、语法分析替换、模块依赖等等。
最后,根据模块的依赖(import xxx from xxx)进行遍历,递归创建子模块。
