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
)进行遍历,递归创建子模块。