webpack源码阅读之主流程分析
comipler是其webpack的支柱模块,其继承于Tapable类,在compiler上定义了很多钩子函数,贯穿其整个编译流程,这些钩子上注册了很多插件,用于在特定的时机执行特定的操作,同时,用户也可以在这些钩子上注册自定义的插件来进行功能拓展,接下来将围绕这些钩子函数来分析webpack的主流程。
1. compiler实例化
compiler对象的生成过程大致可以简化为如下过程,首先对我们传入的配置进行格式验证,接着调用Compiler构造函数生成compiler实例,自定义的plugins注册,最后调用new WebpackOptionsApply().process(options, compiler)
进行默认插件的注册,comailer初始化等。
const webpack = (options,callback)=>{
//options格式验证
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
...
//生成compiler对象
let compiler = new Compiler(options.context);
//自定义插件注册
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
//默认插件注册,默认配置等
compiler.options = new WebpackOptionsApply().process(options, compiler);
}
Webpackoprionapply
是一个重要的步骤,通常是此处插件注册在compiler.hooks.thisCompilation或compiler.hooks.compilation上,并在compilation钩子上调用时,进一步注册到parser(用于生成依赖及依赖模版)或者mainTemplate(用于seal阶段render)的钩子上:
process(options, compiler) {
//当target是一个函数时,可以自定义该环境下使用哪些plugins
if (typeof options.target === "string") {
//1.不同target下引入不同的plugin进行文件加载
switch (options.target) {
case "web":
//JsonpTemplatePlugin插件注册在compiler.hooks.this.compilation上,并在该钩子调用时,在compilation.mainTemplate的多个钩子上注册事件以在最后生成的代码中加入Jsonp Script进行文件加载
new JsonpTemplatePlugin().apply(compiler);
new FetchCompileWasmTemplatePlugin({
mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);
//在compiler.hooks.compilation上注册,并挂载在compilation.moduleTemplates.javascript上,在seal阶段template.hooks.render时调用
new FunctionModulePlugin().apply(compiler);
new NodeSourcePlugin(options.node).apply(compiler);
new LoaderTargetPlugin(options.target).apply(compiler);
break;
case "node":
case "async-node":
//如果目标环境为node,可以用require方式加载文件,而不需要使用Jsonp
new NodeTemplatePlugin({
asyncChunkLoading: options.target === "async-node"
}).apply(compiler);
new ReadFileCompileWasmTemplatePlugin({
mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);
new FunctionModulePlugin().apply(compiler);
new NodeTargetPlugin().apply(compiler);
new LoaderTargetPlugin("node").apply(compiler);
break;
...........
}
//2. output Library处理
...........
//3. devtool sourceMap处理
...........
//注册在compiler.hooks.compilation上,给normalModuleFactory的js模块提供Parser、JavascriptGenerator对象 ,并给seal阶段的template提供renderManifest数组(包含render方法)
new JavascriptModulesPlugin().apply(compiler);
//注册在compiler.hooks.compilation上,给normalModuleFactory的jso n模块提供Parser、JavascriptGenerator对象
new JsonModulesPlugin().apply(compiler);
//同理,webassembly模块
new WebAssemblyModulesPlugin({
mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);
//4. 入口不同格式下的处理,注册在compiler.hooks.entryOption,在调用时新建SingleEntryPlugin或MultiEntryPlugin
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
//5. 不同模块写法的处理,一般注册在compiler.hooks.compilation上,调用时在normalModuleFactory.hooks.parse上注册,接着在parse的hooks上注册,在parse阶段,遇到不同的节点调用不同的plugin,从而在模块的dependencies数组中推入不同的dependencyFactory和dependencyTemplate
new CompatibilityPlugin().apply(compiler);
//es模块
new HarmonyModulesPlugin(options.module).apply(compiler);
if (options.amd !== false) {
//AMD模块
const AMDPlugin = require("./dependencies/AMDPlugin");
const RequireJsStuffPlugin = require("./RequireJsStuffPlugin");
new AMDPlugin(options.module, options.amd || {}).apply(compiler);
new RequireJsStuffPlugin().apply(compiler);
}
//CommonJS模块
new CommonJsPlugin(options.module).apply(compiler);
new LoaderPlugin().apply(compiler);
if (options.node !== false) {
const NodeStuffPlugin = require("./NodeStuffPlugin");
new NodeStuffPlugin(options.node).apply(compiler);
}
new ImportPlugin(options.module).apply(compiler);
new SystemPlugin(options.module).apply(compiler);
.........
//6. 优化
.........
//7. modeId、chunkId相关
.........
//8. resolve初始配置,在resolve时调用this.getResolver时调用
compiler.resolverFactory.hooks.resolveOptions
.for("normal")
.tap("WebpackOptionsApply", resolveOptions => {
return Object.assign(
{
fileSystem: compiler.inputFileSystem
},
cachedCleverMerge(options.resolve, resolveOptions)
);
});
compiler.resolverFactory.hooks.resolveOptions
.for("context")
.tap("WebpackOptionsApply", resolveOptions => {
return Object.assign(
{
fileSystem: compiler.inputFileSystem,
resolveToContext: true
},
cachedCleverMerge(options.resolve, resolveOptions)
);
});
compiler.resolverFactory.hooks.resolveOptions
.for("loader")
.tap("WebpackOptionsApply", resolveOptions => {
return Object.assign(
{
fileSystem: compiler.inputFileSystem
},
cachedCleverMerge(options.resolveLoader, resolveOptions)
);
});
compiler.hooks.afterResolvers.call(compiler);
return options;
}
2. compiler.run
生成compler实例后,cli.js中就会调用compiler.run方法了,compiler.run的流程大致可以简写如下(去掉错误处理等逻辑),其囊括了整个打包过程,首先依次触发beforeRun、run等钩子,接下来调用compiler.compile()进行编译过程,在回调中取得编译后的compilation对象,调用compiler.emitAssets()输出打包好的文件,最后触发done钩子。
run(){
const onCompiled = (err, compilation) => {
//打包输出
this.emitAssets(compilation, err => {
this.hooks.done.callAsync(stats)
};
// beforeRun => run => this.compile()
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.readRecords(err => {
this.compile(onCompiled);
});
});
});
}
3. compiler.compile
在这个方法中主要也是通过回调触发钩子进行流程控制,通过newCompilation=>make=>finsih=>seal
流程来完成一次编译过程,compiler将具体一次编译过程放在了compilation实例上,可以将主流程与编译过程分割开来,当处于watch模式时,可以进行多次编译。
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
});
});
});
});
}
从图中可以看到make钩子上注册了singleEntryPlugin(单入口配置时),compilation作为参数传入该插件,接着在插件中调用compilation.addEntry方法开始编译过程。
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
4. compilation过程
编译过程的入口在compilation._addModuleChain函数,传入entry,context参数,在回调中得到编译生成的module。编译的过程包括文件和loader路径的resolve,loader对源文件的处理,递归的进行依赖处理等等.
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name);
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
}
);
}
在this._addModuleChain
中调用moduleFactory.create()
来开始模块的创建,模块创建第一步需要通过resolve得到入口文件的具体路径。
4.1 resolve
webpack 中每涉及到一个文件,就会经过 resolve 的过程。webpack
使用 enhanced-resolve 来提供绝对路径、相对路径、模块路径的多样解析方式。
在moduleFactory.create()
resolve过程从通过调用normalModuleFactory中factory函数开始。
factory(result, (err, module) => {
if (err) return callback(err);
if (module && this.cachePredicate(module)) {
for (const d of dependencies) {
dependencyCache.set(d, module);
}
}
callback(null, module);
});
//传入的result的基本形式如下
result = {
context: "/Users/hahaha/project/demo/webpack-demo"
contextInfo: {issuer: "", compiler: undefined}
dependencies: [SingleEntryDependency]
request: "./src/index.js"
resolveOptions: {}
}
factory方法拿到入口信息result后,将result传递给resolver方法,resolver方法先得到对普通文件和loader文件的resolve方法:
const loaderResolver = this.getResolver("loader");
const normalResolver = this.getResolver("normal", data.resolveOptions);
然后检查路径中是否包含内联loaders, 通过调用loaderResolver和normalResolver并行的resolve文件路径和内联loaders路径。如果使用了内联loaders,则将其保存在loaders变量中,接着对得到的文件路径进行ruler匹配,得到匹配到的loader数值:
const result = this.ruleSet.exec({
resource: resourcePath,
realResource:
matchResource !== undefined
? resource.replace(/\?.*/, "")
: resourcePath,
resourceQuery,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler
});·
this.ruleSet
是用户定义的loaders和默认loader的格式化的结果,通过其exec方法可以得到与资源文件匹配的loaders数组。
接下来并行的resolver这些loaders路径,并保存在loaders数组中;值得注意的是,在resolver钩子的回调中初始化了parser和generator对象:
parser: this.getParser(type, settings.parser),
generator: this.getGenerator(type, settings.generator),
parser的注册方法如下(webpackOptionapply中各种模块处理的插件就是这样注册的):
compiler.hooks.normalModuleFactory.tap('MyPlugin', factory => {
factory.hooks.parser.for('javascript/auto').tap('MyPlugin', (parser, options) => {
parser.hooks.someHook.tap(/* ... */);
});
});
入口文件通过resolver后得到的结果类似如下(没使用loaders,所以为空数组):
{
context: "/Users/hahaha/project/demo/webpack-demo"
dependencies: [SingleEntryDependency]
generator: JavascriptGenerator {}
loaders: []
matchResource: undefined
parser: Parser {_pluginCompat: SyncBailHook, hooks: {…}, options: {…}, sourceType: "auto", scope: undefined, …}
rawRequest: "./src/index.js"
request: "/Users/hahaha/project/demo/webpack-demo/src/index.js"
resolveOptions: {}
resource: "/Users/hahaha/project/demo/webpack-demo/src/index.js"
resourceResolveData: {
context: {…},
path: "/Users/hahaha/project/demo/webpack-demo/src/index.js",
request: undefined,
query: "",
module: false, …
}
settings: {type: "javascript/auto", resolve: {…}}
type: "javascript/auto"
userRequest: "/Users/hahaha/project/demo/webpack-demo/src/index.js"
}
resolver过程得到的入口文件的路径,接下来在factory方法中会调用createdModule = new NormalModule(result)
生成模块,改构造函数将resolver得到的信息保存到模块上,并提供了一些实例方法来进行后续的build过程。
本文并未深入resolve具体流程,详情可以参阅:
webpack系列之三resolve
4.2 build
在moduleFactory.create()
方法的回调中得到resolve后生成的module后,将开始模块的build过程,我将代码主干保留如下:
moduleFactory.create(
{
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
},
(err, module) => {
const afterBuild = () => {
if (addModuleResult.dependencies) {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
} else {
return callback(null, module);
}
};
this.buildModule(module, false, null, null, err => {
afterBuild();
});
}
);
首先调用this.buildModule
方法,由于moduleFactory.create()
生成的module是normalModule(本例中)的实例,所以可以实际上是调用normalModule.doBuild()
进行build,可以看到首先生成了一个loaderContext对象,在后面运行loader的时候,会通过call方法将loader的this指向loaderContext。
doBuild(options, compilation, resolver, fs, callback) {
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
...
return callback();
}
);
}
接下来就进入runloaders方法了,传入的参数包括模块路径,模块的loaders数组,loaderContext等。在该方法内,首先对相关参数进行初始化的操作,特别是将 loaderContext 上的部分属性改写为 getter/setter 函数,这样在不同的 loader 执行的阶段可以动态的获取一些参数。接下来进入iteratePitchingLoaders
方法:
function iteratePitchingLoaders(options, loaderContext, callback) {
//当处理完最后一个loader的pitch后,倒序开始处理loader的normal方法
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// iterate
if(currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
// 在loadLoader中,通过module = require(loader.path)加载loader,并将module上的normal、pitch、raw属性拷贝到loader对象上
loadLoader(currentLoaderObject, function(err) {
if(err) {
loaderContext.cacheable(false);
return callback(err);
}
var fn = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
//如果没有该loader上没有pitch,则跳到下一个loader的pitch
if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
//在runSyncOrAsync内执行loader上的pitch函数
runSyncOrAsync(
fn,
loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) {
if(err) return callback(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);
}
}
);
});
}
在深入runSyncOrAsync
函数之前,我们先来介绍下webpack官网上的loader API:
-
同步loader
//当不返回map,meta时可以直接返回 module.exports = function(content,map,meta){ return someSyncOperation(content) } //返回多个参数时,要通过this.callback调用 module.exports = function(content,map,meta){ this.callback(null,someSyncOperation(content),map,meta) return }
-
异步loader
//对于异步loader,使用this.async来获取callback函数 module.exports = function(content,map,meta){ let callback = this.async(); someAsyncOperation(content,function(err,result,sourceMap,meta){ if(err) return callback(err); callback(null,result,sourceMap,meta); }) } //promise写法 module.exports = function(content){ return new Promise(resolve =>{ someAsyncOperation(content,(err,result)=>{ if(err) resolve(err) resolve(null,result) }) }) }
了解了loader的写法后,我们在来看看loader的执行函数runSyncOrAsync
:
function runSyncOrAsync(fn, context, args, callback) {
var isSync = true;
var isDone = false;
var isError = false; // internal error
var reportedError = false;
context.async = function async() {
if(isDone) {
if(reportedError) return; // ignore
throw new Error("async(): The callback was already called.");
}
isSync = false;
return innerCallback;
};
var innerCallback = context.callback = function() {
if(isDone) {
if(reportedError) return; // ignore
throw new Error("callback(): The callback was already called.");
}
isDone = true;
isSync = false;
try {
callback.apply(null, arguments);
} catch(e) {
isError = true;
throw e;
}
};
try {
var result = (function LOADER_EXECUTION() {
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);
}
return callback(null, result);
}
} catch(e) {
if(isError) throw e;
if(isDone) {
// loader is already "done", so we cannot use the callback function
// for better debugging we print the error on the console
if(typeof e === "object" && e.stack) console.error(e.stack);
else console.error(e);
return;
}
isDone = true;
reportedError = true;
callback(e);
}
}
结合loader执行的各种写法,runSyncOrAsync
的逻辑就很清晰了。
我们知道,loader上的方法有pitch和normal之分,它们都是用runSyncOrAsync
执行的,执行顺序为:
//config中
use: [
'a-loader',
'b-loader',
'c-loader'
]
//执行顺序
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
loader的pitch方法一般写法如下:
//data对象保存在loaderContext对象的data属性中,可以用于在循环时,捕获和共享前面的信息。
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
data.value = 42;
};
//pitch中有返回值时,会跳过后续的pitch和内层的normal方法
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
if (someCondition()) {
return 'module.exports = require(' + JSON.stringify('-!' + remainingRequest) + ');';
}
};
例如在style-loader和css-loader一起使用时,先执行的style-loader的pitch方法,返回值如下:
var content = require("!!../node_modules/css-loader/dist/cjs.js!./style.css");
if (typeof content === 'string') {
content = [[module.id, content, '']];
}
var options = {}
options.insert = "head";
options.singleton = false;
var update = require("!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js")(content, options);
if (content.locals) {
module.exports = content.locals;
}
由于有返回值,会跳过后续的style-loader的pitch方法、css-loader的pitch方法、css-loader的normal方法和css-loader的normal方法。然后在后续处理依赖时处理内联loader的时候再进行css-loader的处理。
loaders处理之后,得到处理后的文件的内容字符串保存在module的_source变量中,如何从这个字符串中得到依赖呢?这就需要对这个字符串进行处理了,在回调函数中this.parser.parse
方法被执行:
parse(source, initialState) {
let ast;
let comments;
if (typeof source === "object" && source !== null) {
ast = source;
comments = source.comments;
} else {
comments = [];
ast = Parser.parse(source, {
sourceType: this.sourceType,
onComment: comments
});
}
const oldScope = this.scope;
const oldState = this.state;
const oldComments = this.comments;
this.scope = {
topLevelScope: true,
inTry: false,
inShorthand: false,
isStrict: false,
definitions: new StackedSetMap(),
renames: new StackedSetMap()
};
const state = (this.state = initialState || {});
this.comments = comments;
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectStrictMode(ast.body);
this.prewalkStatements(ast.body);
this.blockPrewalkStatements(ast.body);
this.walkStatements(ast.body);
}
this.scope = oldScope;
this.state = oldState;
this.comments = oldComments;
return state;
}
先调用Parse.parse方法得到AST,然后就是对这个树进行遍历了,流程为: program事件 -> detectStrictMode -> prewalkStatements -> walkStatements。这个过程会通过遍历AST的各个节点,从而触发不同的钩子函数,在这些钩子函数上会触发一些模块处理的方法(这些方法大多是在webpackOptionapply中注册到parser上的)给 module 增加很多 dependency 实例,每个 dependency 类都会有一个 template 方法,并且保存了原来代码中的字符位置 range,在最后生成打包后的文件时,会用 template 的结果替换 range 部分的内容。
所以最终得到的 dependency 不仅包含了文件中所有的依赖信息,还被用于最终生成打包代码时对原始内容的修改和替换,例如将 return 'sssss' + A
替换为 return 'sssss' + _a_js__WEBPACK_IMPORTED_MODULE_0__["A"]
program 事件中,会触发两个 plugin 的回调:HarmonyDetectionParserPlugin 和 UseStrictPlugin
HarmonyDetectionParserPlugin中,如果代码中有 import 或者 export 或者类型为 javascript/esm,那么会增加了两个依赖:HarmonyCompatibilityDependency, HarmonyInitDependency 依赖。
UseStrictPlugin用来检测文件是否有 use strict
,如果有,则增加一个 ConstDependency 依赖。
整个 parse 的过程关于依赖的部分,我们总结一下:
- 将 source 转为 AST(如果 source 是字符串类型)
- 遍历 AST,遇到 import 语句就增加相关依赖,代码中出现 A(import 导入的变量) 的地方也增加相关的依赖。
所有的依赖都被保存在 module.dependencies 中。module.dependencies大致内容如下:
0:CommonJsRequireDependency
loc: SourceLocation
end: Position {line: 1, column: 77}
start: Position {line: 1, column: 14}
__proto__: Object
module: null
optional: false
range: (2) [22, 76]
request: "!!../node_modules/css-loader/dist/cjs.js!./style.css"
userRequest: "!!../node_modules/css-loader/dist/cjs.js!./style.css"
weak: false
type: (...)
1: RequireHeaderDependency {module: null, weak: false, optional: false, loc: SourceLocation, range: Array(2)}
2: ConstDependency {module: null, weak: false, optional: false, loc: SourceLocation, expression: "module.i", …}
3: CommonJsRequireDependency {module: null, weak: false, optional: false, loc: SourceLocation, request: "!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js", …}
4: RequireHeaderDependency {module: null, weak: false, optional: false, loc: SourceLocation, range: Array(2)}
如图中所示,接下来就是处理依赖了,进入回调中的processModuleDependencies
方法:
processModuleDependencies(module, callback) {
const dependencies = new Map();
const addDependency = dep => {
const resourceIdent = dep.getResourceIdentifier();
// 过滤掉没有 ident 的,就是请求路径,例如 constDependency 这些只用在最后打包文件生成的依赖
if (resourceIdent) {
const factory = this.dependencyFactories.get(dep.constructor);
if (factory === undefined) {
throw new Error(
`No module factory available for dependency type: ${dep.constructor.name}`
);
}
let innerMap = dependencies.get(factory);
if (innerMap === undefined) {
dependencies.set(factory, (innerMap = new Map()));
}
let list = innerMap.get(resourceIdent);
if (list === undefined) innerMap.set(resourceIdent, (list = []));
list.push(dep);
}
};
const addDependenciesBlock = block => {
if (block.dependencies) {
iterationOfArrayCallback(block.dependencies, addDependency);
}
if (block.blocks) {
iterationOfArrayCallback(block.blocks, addDependenciesBlock);
}
if (block.variables) {
iterationBlockVariable(block.variables, addDependency);
}
};
try {
addDependenciesBlock(module);
} catch (e) {
callback(e);
}
const sortedDependencies = [];
for (const pair1 of dependencies) {
for (const pair2 of pair1[1]) {
sortedDependencies.push({
factory: pair1[0],
dependencies: pair2[1]
});
}
}
this.addModuleDependencies(
module,
sortedDependencies,
this.bail,
null,
true,
callback
);
}
接下来进入this.addModuleDependencies,在该函数中,递归进行之前的resolve=》buildMoudule过程直到所有的依赖处理完成,到此build过程就完成了。
详情参阅https://juejin.im/post/5cc51b...
4.3 compilation.seal
在上一步build完成后,build好的module保存在compilation._modules对象中,接下来需要根据这些modules生成chunks,并生成最后打包好的代码保存到compilation.assets中。
去除优化的钩子和一些支线剧情,seal方法可以简写如下:
seal(callback) {
this.hooks.seal.call();
// 初始化chunk、chunkGroups等
for (const preparedEntrypoint of this._preparedEntrypoints) {
const module = preparedEntrypoint.module;
const name = preparedEntrypoint.name;
const chunk = this.addChunk(name);
const entrypoint = new Entrypoint(name);
entrypoint.setRuntimeChunk(chunk);
entrypoint.addOrigin(null, name, preparedEntrypoint.request);
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint);
//在chunkGroups的chunk数组中推入chunk,在chunk的_groups Set中加入chunhGroups,建立两者联系
GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
//在module的_chunks Set中加入chunk,chunk的_modules Set中加入module
GraphHelpers.connectChunkAndModule(chunk, module);
chunk.entryModule = module;
chunk.name = name;
//给各个依赖的module按照引用层级加上depth属性,如入口为的depth为0
this.assignDepth(module);
}
//生成module graph 和chunk graph
buildChunkGraph(
this,
/** @type {Entrypoint[]} */ (this.chunkGroups.slice())
);
this.sortModules(this.modules);
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
this.hooks.beforeModuleIds.call(this.modules);
this.hooks.moduleIds.call(this.modules);
this.applyModuleIds();
this.hooks.beforeChunkIds.call(this.chunks);
this.applyChunkIds();
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
return this.hooks.afterSeal.callAsync(callback);
});
}
首先是在compilation对象上初始化chunk、chunkGroups等变量,利用GraphHelpers
方法建立module和chunk,chunk和chunkGroups之间的关系,调用assignDepth
方法给每个module加上依赖层级depth,接着进入buildChunkGraph
生成chunk graph。
const buildChunkGraph = (compilation, inputChunkGroups) => {
// SHARED STATE
/** @type {Map} */
const chunkDependencies = new Map();
/** @type {Set} */
const allCreatedChunkGroups = new Set();
/** @type {Map} */
const chunkGroupInfoMap = new Map();
/** @type {Set} */
const blocksWithNestedBlocks = new Set();
// PART ONE
visitModules(
compilation,
inputChunkGroups,
chunkGroupInfoMap,
chunkDependencies,
blocksWithNestedBlocks,
allCreatedChunkGroups
);
// PART TWO
connectChunkGroups(
blocksWithNestedBlocks,
chunkDependencies,
chunkGroupInfoMap
);
// Cleaup work
cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
};
主要逻辑在visitModules
方法中,首先通过const blockInfoMap = extraceBlockInfoMap(compilation)
得到module graph,module是一个Map,键名是各个module,键值是module的依赖,分为异步加载的依赖blocks和同步依赖modules:
0: {NormalModule => Object}
key: NormalModule {dependencies: Array(4), blocks: Array(1), variables: Array(0), type: "javascript/auto", context: "/Users/hahaha/project/demo/webpack-demo/src", …}
value:
blocks: [ImportDependenciesBlock]
modules: Set(1) {NormalModule}
1: {ImportDependenciesBlock => Object}
2: {NormalModule => Object}
3: {NormalModule => Object}
4: {NormalModule => Object}
5: {NormalModule => Object}
6: {NormalModule => Object}
然后利用两层循环将栈内的模块及其依赖一层层的加入到chunk的this._modules对象中,同步依赖放在内层循环处理,异步依赖放在外层循环处理。(利用栈处理递归依赖以及利用swtich进行流程管理)
接下来connectChunkGroups
、cleanupUnconnectedGroups
,遍历 chunk graph,通过和依赖的 module 之间的使用关系来建立起不同 chunkGroup 之间的父子关系,同时剔除一些没有建立起联系的 chunk,没细看
详情:webpack系列之六chunk图生成
接下来就是生成module id和chunk id了,之前好像是生成的数字id,现在好像在NamedModulesPlugin和NamedChunksPlugin插件中将id命名成文件名了。
在his.createHash
方法中生成hash,包括本次编译的hash、chunkhash、modulehash。hash的生成步骤基本如下,首先create得到moduleHash
方法,再在updateHash
方法中不断的加各种内容,例如modulehash生成过程中就用到了module id、各种依赖、export信息等,最后调用digest方法生成hash:
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
module.hash = /** @type {string} */ (moduleHash.digest(hashDigest));
module.renderedHash = module.hash.substr(0, hashDigestLength);
chunkhash生成过程中会用到chunk id、module id、name、template信息等。
最后就是调用
createChunkAssets() {
const outputOptions = this.outputOptions;
const cachedSourceMap = new Map();
const alreadyWrittenFiles = new Map();
//遍历chunks数组
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
chunk.files = [];
let source;
let file;
let filenameTemplate;
try {
//入口模块就是hasRuntime,相对于普通模块,加了一层webpack runtime bootstrap 自执行函数包裹
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
//在该函数内会触发相应template.hooks.renderManifest钩子,在webpackoptionapply中注册的javaScriptModulesPlugin(一般是这个)中执行逻辑,在返回结果中推入render方法。
const manifest = template.getRenderManifest({
chunk,
hash: this.hash,
fullHash: this.fullHash,
outputOptions,
moduleTemplates: this.moduleTemplates,
dependencyTemplates: this.dependencyTemplates
}); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]
for (const fileManifest of manifest) {
//缓存处理
........
//调用上一步得到的render方法
source = fileManifest.render();
}
this.assets[file] = source;
chunk.files.push(file);
this.hooks.chunkAsset.call(chunk, file);
alreadyWrittenFiles.set(file, {
hash: usedHash,
source,
chunk
});
}
}
}
}
当为chunkTemplate时,javaScriptModulesPlugin中的render方法:
renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
//获取每个 chunk 当中所依赖的所有 module 最终需要渲染的代码
const moduleSources = Template.renderChunkModules(
chunk,
m => typeof m.source === "function",
moduleTemplate,
dependencyTemplates
);
//最终生成 chunk 代码前对 chunk 最修改
const core = chunkTemplate.hooks.modules.call(
moduleSources,
chunk,
moduleTemplate,
dependencyTemplates
);
//外层添加包裹函数
let source = chunkTemplate.hooks.render.call(
core,
chunk,
moduleTemplate,
dependencyTemplates
);
if (chunk.hasEntryModule()) {
source = chunkTemplate.hooks.renderWithEntry.call(source, chunk);
}
chunk.rendered = true;
return new ConcatSource(source, ";");
}
moduleSources示例:
{
,
/***/ "./src/foo.js":
,/*!********************!*\
, !*** ./src/foo.js ***!
, \********************/
,/*! exports provided: default */
,/***/ (function(module, __webpack_exports__, __webpack_require__) {
,"use strict";
,"eval("__webpack_require__.r(__webpack_exports__);\n
/* harmony default export */
__webpack_exports__[\"default\"] =
(function(){\n
console.log('here are foo')\n
}
);
\n\n\n//# sourceURL=webpack:///./src/foo.js?");",
/***/
})
最后得到source的示例:
"(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],,{
,
/***/ "./src/foo.js":
,/*!********************!*\
, !*** ./src/foo.js ***!
, \********************/
,/*! exports provided: default */
,/***/ (function(module, __webpack_exports__, __webpack_require__) {
,"use strict";
,"eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (function(){\n console.log('here are foo')\n});\n\n\n//# sourceURL=webpack:///./src/foo.js?");",
/***/ }),
},])"
当为mainTemplate时,调用的是mainTemplate中的render方法如下:
render(hash, chunk, moduleTemplate, dependencyTemplates) {
const buf = this.renderBootstrap(
hash,
chunk,
moduleTemplate,
dependencyTemplates
);
let source = this.hooks.render.call(
new OriginalSource(
Template.prefix(buf, " \t") + "\n",
"webpack/bootstrap"
),
chunk,
hash,
moduleTemplate,
dependencyTemplates
);
if (chunk.hasEntryModule()) {
source = this.hooks.renderWithEntry.call(source, chunk, hash);
}
if (!source) {
throw new Error(
"Compiler error: MainTemplate plugin 'render' should return something"
);
}
chunk.rendered = true;
return new ConcatSource(source, ";");
}
得到的Bootstrap如下:
// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(parentJsonpFunction) parentJsonpFunction(data);
while(resolves.length) {
resolves.shift()();
}
};
,
// The module cache
var installedModules = {};
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
"main": 0
};
// script path function
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + chunkId + ".bundle." + "eecd41ca7ca8f56e3293" + ".js"
},,// The require function,function __webpack_require__(moduleId) {,
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;,},,// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// start chunk loading
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// define __esModule on exports
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// __webpack_public_path__
__webpack_require__.p = "";
// on error function for async loading
__webpack_require__.oe = function(err) { console.error(err); throw err; };,,var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
,// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");"
得到的source如下:
"/******/ (function(modules) { // webpackBootstrap
,[object Object],/******/ })
,/************************************************************************/
,/******/ (,{
,
/***/ "./node_modules/css-loader/dist/cjs.js!./src/style.css":
,/*!*************************************************************!*\
, !*** ./node_modules/css-loader/dist/cjs.js!./src/style.css ***!
, \*************************************************************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {
,[object Object],
/***/ }),,
,
/***/ "./node_modules/css-loader/dist/runtime/api.js":
,/*!*****************************************************!*\
, !*** ./node_modules/css-loader/dist/runtime/api.js ***!
, \*****************************************************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {
,"use strict";
,[object Object],
/***/ }),,
,
/***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js":
,/*!****************************************************************************!*\
, !*** ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ***!
, \****************************************************************************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {
,"use strict";
,[object Object],
/***/ }),,
,
/***/ "./src/index.js":
,/*!**********************!*\
, !*** ./src/index.js ***!
, \**********************/
,/*! no exports provided */
,/***/ (function(module, __webpack_exports__, __webpack_require__) {
,"use strict";
,[object Object],
/***/ }),,
,
/***/ "./src/style.css":
,/*!***********************!*\
, !*** ./src/style.css ***!
, \***********************/
,/*! no static exports found */
,/***/ (function(module, exports, __webpack_require__) {
,[object Object],
/***/ }),
/******/ },)
5. compiler.emitAssets
经历了上面所有的阶段之后,所有的最终代码信息已经保存在了 Compilation 的 assets 中,当 assets 资源相关的优化工作结束后,seal 阶段也就结束了。这时候执行 seal 函数接受到 callback,callback回溯到compiler.run中,执行compiler.emitAssets.
在这个方法当中首先触发 hooks.emit 钩子函数,即将进行写文件的流程。接下来开始创建目标输出文件夹,并执行 emitFiles 方法,将内存当中保存的 assets 资源输出到目标文件夹当中,这样就完成了内存中保存的 chunk 代码写入至最终的文件
参考资料:
https://juejin.im/post/5d4d08...