Webpack源码解析
// 使用webpack版本
"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
打包主流程分析
调用run方法做了什么
1. run
run(callback) {
// 如果有一个 Compilation 进行中,那么就不能开启第二个 Compilation
// 因为webpack一次只允许有且仅有一个Compilation运行
if (this.running) return callback(new ConcurrentCompilationError());
// 运行完毕执行回调(如果存在回调)
const finalCallback = (err, stats) => {
// 表示一个Compilation已经完成
this.running = false;
// 如果Compilation失败调用failed钩子
if (err) {
this.hooks.failed.call(err);
}
// 成功后存在回调就执行它
if (callback !== undefined) return callback(err, stats);
};
// 记录任务启动时间
const startTime = Date.now();
// 标记一个Compilation任务运行中
this.running = true;
// compiler 完成的回调,最终会产生一个compilation
const onCompiled = (err, compilation) => {
// 如果编译失败就调用finalCallback
if (err) return finalCallback(err);
// 如果不需要输出打包文件,就直接执行done钩子注册的函数,参数是打包统计数据
// 执行完注册的钩子函数后执行最终的回调,将打包统计数据返回
if (this.hooks.shouldEmit.call(compilation) === false) {
const stats = new Stats(compilation);
stats.startTime = startTime;
stats.endTime = Date.now();
this.hooks.done.callAsync(stats, err => {
if (err) return finalCallback(err);
return finalCallback(null, stats);
});
return;
}
// 调用输出assets方法,参数就是生成的compilation和最终回调
this.emitAssets(compilation, err => {
// 处理错误
if (err) return finalCallback(err);
// 处理二次打包
if (compilation.hooks.needAdditionalPass.call()) {
// 如果需要二次打包,标注为true
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);
this.compile(onCompiled);
});
});
return;
}
// 不需要二次打包的话,就调用emitRecords记录输出
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);
});
});
});
};
// 触发run之前的钩子,执行run之前注册的任务,参数是compiler实例
// 在webpack/lib/node/NodeEnvironmentPlugin.js里面会在这个钩子上注册一个事件:
// compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
// if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
// });
// 主要就是重置compiler的inputFileSystem,为下一次的compiler做准备
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
// 执行beforeRun完毕时立即执行run钩子上注册的任务
// 在webpack/lib/CachePlugin.js里面会在这个run钩子上注册一个事件:
// compiler.hooks.run.tapAsync("CachePlugin", (compiler, callback) => {
// if (!compiler._lastCompilationFileDependencies) {
// return callback();
// }
// ...
// });
// 主要是处理上一次compilation的缓存
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
// run钩子上注册的任务也执行完毕时读取记录进行编译
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
}
run 也就是进行编译,生成 compilation ,run之前做了两个事情,也就是注册在 beforeRun上的重置 inputFileSystem 和注册在 run 上的用来处理缓存的 CachePlugin。这两件事情做完了后又调用了 readRecords 方法,然后才进行编译,下面我们就看一下 readRecords 方法。
2. readRecords
开启这个选项可以生成一个 JSON 文件,其中含有 webpack 的 "records" 记录 - 即「用于存储跨多次构建(across multiple builds)的模块标识符」的数据片段。可以使用此文件来跟踪在每次构建之间的模块变化。只要简单的设置一下路径,就可以生成这个 JSON 文件:
如果你使用了代码分离(code splittnig)这样的复杂配置,records 会特别有用。这些数据用于确保拆分 bundle,以便实现你需要的缓存(caching)行为。
注意,虽然这个文件是由编译器(compiler)生成的,但你可能仍然希望在源代码管理中追踪它,以便随时记录它的变化情况。
设置 recordsPath 本质上会把 recordsInputPath 和 recordsOutputPath 都设置成相同的路径。通常来讲这也是符合逻辑的,除非你决定改变记录文件的名称。
综上所述,records即上次编译打包的模块信息,为后续编译缓存做好准备,也能更方便、更准确地发现模块的变化。
首先我们需要配置records的路径 recordsPath: path.resolve(__dirname, './recordsPath.json'),这样在我们打包过后就会生成records记录文件:
{
"HtmlWebpackCompiler": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
后续我们就可以在 compiler 之前获取到上一次编译的模块信息。
/**
* 读取编译记录
* @param {*} callback 也就是执行compiler
* @returns compiler的返回值
*/
readRecords(callback) {
// 如果不存在records json文件或者未启用record,就设置一个空的record,执行compiler
if (!this.recordsInputPath) {
this.records = {};
return callback();
}
// 如果存在路径,就检验路径是否有效(防止文件被删)
this.inputFileSystem.stat(this.recordsInputPath, err => {
// 如果无效直接执行compiler
if (err) return callback();
// 路径有效就读取record json
this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
// 读取失败抛出失败信息,不进行compiler
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);
}
// 转换正常,records被正常赋值,执行compiler
// callback ---> this.compile(onCompiled);
return callback();
});
});
}
3. compile
records 读取完毕就执行 compiler,下面我们看一下这个 compile 方法:
// node_modules/webpack/lib/Compiler.js
createCompilation() {
// 返回一个compilation实例
return new Compilation(this);
}
newCompilation(params) {
// 生成compilation实例
const compilation = this.createCompilation();
// 文件时间戳
compilation.fileTimestamps = this.fileTimestamps;
// 上下文时间戳(文件依赖关系)
compilation.contextTimestamps = this.contextTimestamps;
// 当前compilation名称(compiler的名称)
compilation.name = this.name;
// 上一次打包的记录
compilation.records = this.records;
// 编译依赖
compilation.compilationDependencies = params.compilationDependencies;
// 执行初始化compilation钩子上注册的任务
this.hooks.thisCompilation.call(compilation, params);
// 执行compilation钩子上注册的任务
this.hooks.compilation.call(compilation, params);
// 返回这个compilation
return compilation;
}
createNormalModuleFactory() {
const normalModuleFactory = new NormalModuleFactory(
this.options.context,
this.resolverFactory,
this.options.module || {}
);
this.hooks.normalModuleFactory.call(normalModuleFactory);
return normalModuleFactory;
}
createContextModuleFactory() {
const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
this.hooks.contextModuleFactory.call(contextModuleFactory);
return contextModuleFactory;
}
newCompilationParams() {
const params = {
// 各类模块
normalModuleFactory: this.createNormalModuleFactory(),
// 模块依赖关系
contextModuleFactory: this.createContextModuleFactory(),
// 打包依赖
compilationDependencies: new Set()
};
return params;
}
compile(callback) {
// 生成编译参数
const params = this.newCompilationParams();
// 执行编译前钩子上注册的任务
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
// 执行compile钩子上注册的任务
this.hooks.compile.call(params);
// 生成一个compilation
const compilation = this.newCompilation(params);
// 执行make钩子上注册的异步任务,参数是这个compilation
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
// 调用compilation上的finish方法,结束编译
compilation.finish(err => {
if (err) return callback(err);
// 封装compilation结果
compilation.seal(err => {
if (err) return callback(err);
// 调用编译后钩子上注册的任务
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
// 最终调用onCompiled进行二次打包或者输出打包结果,整个compiler结束
return callback(null, compilation);
});
});
});
});
});
}
首先会生成一个编译参数,主要存放模块和模块的依赖关系、编译依赖:
{
normalModuleFactory: NormalModuleFactory {
_pluginCompat: SyncBailHook {
_args: [Array],
taps: [Array],
interceptors: [],
call: [Function: lazyCompileHook],
promise: [Function: lazyCompileHook],
callAsync: [Function: lazyCompileHook],
_x: undefined
},
hooks: {
resolver: [SyncWaterfallHook],
factory: [SyncWaterfallHook],
beforeResolve: [AsyncSeriesWaterfallHook],
afterResolve: [AsyncSeriesWaterfallHook],
createModule: [SyncBailHook],
module: [SyncWaterfallHook],
createParser: [HookMap],
parser: [HookMap],
createGenerator: [HookMap],
generator: [HookMap]
},
resolverFactory: ResolverFactory {
_pluginCompat: [SyncBailHook],
hooks: [Object],
cache1: [WeakMap],
cache2: Map {}
},
ruleSet: RuleSet {
references: {},
rules: [Array]
},
cachePredicate: [Function: bound Boolean],
context: '',
parserCache: {},
generatorCache: {}
},
contextModuleFactory: ContextModuleFactory {
_pluginCompat: SyncBailHook {
_args: [Array],
taps: [Array],
interceptors: [],
call: [Function: lazyCompileHook],
promise: [Function: lazyCompileHook],
callAsync: [Function: lazyCompileHook],
_x: undefined
},
hooks: {
beforeResolve: [AsyncSeriesWaterfallHook],
afterResolve: [AsyncSeriesWaterfallHook],
contextModuleFiles: [SyncWaterfallHook],
alternatives: [AsyncSeriesWaterfallHook]
},
resolverFactory: ResolverFactory {
_pluginCompat: [SyncBailHook],
hooks: [Object],
cache1: [WeakMap],
cache2: Map {}
}
},
compilationDependencies: Set {}
}
compile 做了三件核心事情: 生成compilation(模块和模块的依赖关系)、make(根据模块和模块的依赖关系进行递归编译)、seal(封装编译结果,生成最终输出)
4. Compilation实例
Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
Compilation 和 Compiler 一样继承至 Tapable,一开始同样先执行 Tapable 的构造函数,生成插件兼容的钩子,然后挂载钩子 hooks,大体包括 加载入口文件、打包、依赖分析、编译结果封存、代码chunk、优化等等功能钩子。
新的 Compilation 生成完毕就会执行 compiler 上的 make 钩子进行编译,以下就是 make 钩子的执行调用栈:
// VM46947640
(function anonymous(compilation, _callback) {
"use strict";
var _context;
var _x = this._x;
do {
var _counter = 2;
var _done = () => {
_callback();
};
if (_counter <= 0) break;
var _fn0 = _x[0];
_fn0(compilation, (_err0) => {
if (_err0) {
if (_counter > 0) {
_callback(_err0);
_counter = 0;
}
} else {
if (--_counter === 0) _done();
}
});
if (_counter <= 0) break;
var _fn1 = _x[1];
_fn1(compilation, (_err1) => {
if (_err1) {
if (_counter > 0) {
_callback(_err1);
_counter = 0;
}
} else {
if (--_counter === 0) _done();
}
});
} while (false);
});
5. make
make 钩子在一个新的 compilation 生成完毕后执行,根据 compilation 进行编译工作,从上面看一共注册了两个任务。
1) 第一个注册钩子函数 PersistentChildCompilerSingletonPlugin 主要处理子编译,最终将结果交由主编译优化、输出,这里主要就是打包HTML模板:
// node_modules/html-webpack-plugin/lib/cached-child-compiler.js
compiler.hooks.make.tapAsync(
"PersistentChildCompilerSingletonPlugin",
(mainCompilation, callback) => {
// 如果子编译已经进行,或者正在验证缓存,不可以再次开启
if (
this.compilationState.isCompiling ||
this.compilationState.isVerifyingCache
) {
return callback(new Error("Child compilation has already started"));
}
// 更新当前编译的开始时间
compilationStartTime = new Date().getTime();
// 编译开始 - 现在不再可能添加新模板
// entries --> 0:'/Users/***/lagou-edu/webpack-entry/node_modules/html-webpack-plugin/lib/loader.js!/Users/***/lagou-edu/webpack-entry/src/index.html'
// 这里是使用 html-webpack-plugin 加载HTML模板
this.compilationState = {
isCompiling: false,
isVerifyingCache: true,
previousEntries: this.compilationState.compiledEntries,
previousResult: this.compilationState.compilationResult,
entries: this.compilationState.entries,
};
// 检测缓存是否有效,是一个promise
const isCacheValidPromise = this.isCacheValid(
previousFileSystemSnapshot,
mainCompilation
);
let cachedResult = childCompilationResultPromise;
childCompilationResultPromise = isCacheValidPromise.then((isCacheValid) => {
// 如果缓存有效就返回缓存
// 一般我们的html模板不会经常修改,编译缓存就显得非常有意义,能够减少编译时间
if (isCacheValid) {
return cachedResult;
}
// 不存在缓存就开始编译
const compiledEntriesPromise = this.compileEntries(
mainCompilation,
this.compilationState.entries
);
// 子编译完成时立即创建快照,也就是缓存编译结果的依赖关系
compiledEntriesPromise
.then((childCompilationResult) => {
return fileWatcherApi.createSnapshot(
childCompilationResult.dependencies,
mainCompilation,
compilationStartTime
);
})
.then((snapshot) => {
previousFileSystemSnapshot = snapshot;
});
return compiledEntriesPromise;
});
// 将需要监视的文件添加到主编译中
// 在优化依赖树之前调用,插件可以 tap 此钩子执行依赖树优化
// 将编译结果的依赖关系添加到监听,任何依赖发生改变都需要重新优化代码
mainCompilation.hooks.optimizeTree.tapAsync(
"PersistentChildCompilerSingletonPlugin",
(chunks, modules, callback) => {
const handleCompilationDonePromise = childCompilationResultPromise.then(
(childCompilationResult) => {
this.watchFiles(
mainCompilation,
childCompilationResult.dependencies
);
}
);
handleCompilationDonePromise.then(
() => callback(null, chunks, modules),
callback
);
}
);
// 一旦知道主编译哈希,就存储最终编译
mainCompilation.hooks.additionalAssets.tapAsync(
"PersistentChildCompilerSingletonPlugin",
(callback) => {
const didRecompilePromise = Promise.all([
childCompilationResultPromise,
cachedResult,
]).then(([childCompilationResult, cachedResult]) => {
// 子编译发生改变就重新编译
return cachedResult !== childCompilationResult;
});
const handleCompilationDonePromise = Promise.all([
childCompilationResultPromise,
didRecompilePromise,
]).then(([childCompilationResult, didRecompile]) => {
// 一旦发现子编译发生改变,立即更新主编译hash
if (didRecompile) {
mainCompilationHashOfLastChildRecompile = mainCompilation.hash;
}
// 重置子编译状态
this.compilationState = {
isCompiling: false,
isVerifyingCache: false,
entries: this.compilationState.entries,
compiledEntries: this.compilationState.entries,
compilationResult: childCompilationResult,
mainCompilationHash: mainCompilationHashOfLastChildRecompile,
};
});
handleCompilationDonePromise.then(() => callback(null), callback);
}
);
// 继续编译
callback(null);
}
);
// node_modules/html-webpack-plugin/lib/cached-child-compiler.js
/**
* 编译HTML模板
*
* @private
* @param {WebpackCompilation} mainCompilation
* @param {string[]} entries
* @returns {Promise<ChildCompilationResult>}
*/
compileEntries (mainCompilation, entries) {
const compiler = new HtmlWebpackChildCompiler(entries);
return compiler.compileTemplates(mainCompilation).then((result) => {
return {
// The compiled sources to render the content
compiledEntries: result,
// 是否存在文件依赖,以确定依赖发生改变时是否需要重新编译
dependencies: compiler.fileDependencies,
// 主编译的hash可以用来表示这个编译是在当前编译期间完成的
mainCompilationHash: mainCompilation.hash
};
}, error => ({
// The compiled sources to render the content
error,
dependencies: compiler.fileDependencies,
mainCompilationHash: mainCompilation.hash
}));
}
这个 PersistentChildCompilerSingletonPlugin 钩子注册的任务就是从缓存中查找 HTML 模板的编译结果,没有缓存就重新编译模板,最终注册两个钩子函数,一个是 optimizeTree 将编译结果的依赖关系添加到优化树上,一旦依赖发生改变就需要重新优化编译结果;另一个 additionalAssets 一旦重新编译就更新主编译 hash ,添加到输出。
以上 compileEntries 方法中类 htmlWebpackPlugin 的 compileTemplates 方法就是对 HTML 模板进行编译:
// node_modules/html-webpack-plugin/lib/child-compiler.js
/**
* 该函数将启动模板编译
* 一旦启动就不能再添加模板
*
* @param {WebpackCompilation} mainCompilation
* @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
*/
compileTemplates (mainCompilation) {
// 防止对同一个模板进行多次编译
// 编译被缓存在一个 promise 中
// 如果已经存在则返回
if (this.compilationPromise) {
return this.compilationPromise;
}
// 入口文件只是一个空的辅助作为动态模板
// 需要在“loader.js”中添加
const outputOptions = {
filename: '__child-[name]',
publicPath: mainCompilation.outputOptions.publicPath
};
// 编译名称是HTML模板编译
const compilerName = 'HtmlWebpackCompiler';
// 创建一个额外的子编译器,它接受模板
// 并将其转换为 Node.JS html 工厂。
// 这允许我们在编译期间使用加载器
const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions);
// 文件路径
childCompiler.context = mainCompilation.compiler.context;
// 将模板编译为 nodejs javascript
// 模块被包装成nodejs模块进行捆绑导出,入口模块可以通过require导入模块
new NodeTemplatePlugin(outputOptions).apply(childCompiler);
// 如果您在 Node.js 环境中运行包,则应使用这些插件。如果确保本地模块即使捆绑也能正确加载
new NodeTargetPlugin().apply(childCompiler);
new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
new LoaderTargetPlugin('node').apply(childCompiler);
// 添加模板
this.templates.forEach((template, index) => {
new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
});
this.compilationStartedTimestamp = new Date().getTime();
// 将模板编译作为一个promise,以子编译的方式运行编译,将最终编译结果返回
this.compilationPromise = new Promise((resolve, reject) => {
childCompiler.runAsChild((err, entries, childCompilation) => {
// 提取模板
const compiledTemplates = entries
? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
: [];
// 提取依赖关系
if (entries) {
this.fileDependencies = { fileDependencies: Array.from(childCompilation.fileDependencies), contextDependencies: Arrayfrom(childCompilation.contextDependencies), missingDependencies: Array.from(childCompilation.missingDependencies) };
}
// 子编译出错导致编译失败
if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
...
reject(new Error('Child compilation failed:\n' + errorDetails));
return;
}
// 编译回调错误对象包含错误信息则reject
if (err) {
reject(err);
return;
}
// 否则认为编译成功,返回编译结果
/**
* @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
*/
const result = {};
compiledTemplates.forEach((templateSource, entryIndex) => {
// 多个模板对应多个入口文件
result[this.templates[entryIndex]] = {
content: templateSource,
hash: childCompilation.hash,
entry: entries[entryIndex]
};
});
this.compilationEndedTimestamp = new Date().getTime();
resolve(result);
});
});
return this.compilationPromise;
}
这里就涉及到创建一个子编译的问题,它是通过调用主编译 mainCompilation 上的 createChildCompiler 方法来创建一个子编译任务,因为 Compilation 类复制了 Compiler(this.compiler = compiler;),所以 compiler 全程都会携带 Compiler,拥有 Compiler 上的所有属性、方法:
// node_modules/webpack/lib/Compiler.js
createChildCompiler(
compilation,
compilerName,
compilerIndex,
outputOptions,
plugins
) {
// 生成一个新的Compiler
const childCompiler = new Compiler(this.context);
// 挂载plugins至子编译
if (Array.isArray(plugins)) {
for (const plugin of plugins) {
plugin.apply(childCompiler);
}
}
// 挂载主编译中的hooks至子编译,除了数组中的
// 子编译不存在make
for (const name in this.hooks) {
if (
![
"make",
"compile",
"emit",
"afterEmit",
"invalid",
"done",
"thisCompilation"
].includes(name)
) {
if (childCompiler.hooks[name]) {
childCompiler.hooks[name].taps = this.hooks[name].taps.slice();
}
}
}
// ...设置一些属性
// 读取编译记录,不存在就设置为空数组(因为可能存在多个编译)
const relativeCompilerName = makePathsRelative(this.context, compilerName);
if (!this.records[relativeCompilerName]) {
this.records[relativeCompilerName] = [];
}
if (this.records[relativeCompilerName][compilerIndex]) {
childCompiler.records = this.records[relativeCompilerName][compilerIndex];
} else {
this.records[relativeCompilerName].push((childCompiler.records = {}));
}
// 设置编译配置项
childCompiler.options = Object.create(this.options);
childCompiler.options.output = Object.create(childCompiler.options.output);
for (const name in outputOptions) {
childCompiler.options.output[name] = outputOptions[name];
}
// 设置父编译
childCompiler.parentCompilation = compilation;
// 执行父编译childCompiler钩子上注册的任务
compilation.hooks.childCompiler.call(
childCompiler,
compilerName,
compilerIndex
);
return childCompiler;
}
子编译也是一个新的 Compiler 实例,它拷贝了父编译除了 make、compile、emit、afterEmit、invalid、done、thisCompilation 之外的所有 hooks。
在执行子编译的时候触发了注册在make上的另外一个钩子 SingleEntryPlugin:
// node_modules/webpack/lib/SingleEntryPlugin.js
compiler.hooks.make.tapAsync("SingleEntryPlugin", (compilation, callback) => {
// 这里的entry是htmlWebpackPlugin的load path + html path
// '/Users/***/lagou-edu/webpack-entry/node_modules/html-webpack-plugin/lib/loader.js!/Users/***/lagou-edu/webpack-entry/src/user.html'
// name 就是htmlWebpackPlugin
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
});
这是一个动态添加入口文件的插件(在webpack5中移除了,改为EntryPlugin),将 html 模板作为一个入口文件,最终的编译后 js 和 css 文件都会以标签的形式插入。
2) 第二个注册的钩子函数 SingleEntryPlugin 就是加载入口文件的:
// node_modules/webpack/lib/SingleEntryPlugin.js
compiler.hooks.make.tapAsync("SingleEntryPlugin", (compilation, callback) => {
// 这里的entry就是 ./src/index.js
// name 就是 main
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
});
dep 是创建的入口文件依赖信息,在addEntry时作为entry实参传入:
SingleEntryDependency {module: null, weak: false, optional: false, loc: {…}, request: './src/index.js', …}
▶ loc:{name: 'main'}
module:null
optional:false
request:'./src/index.js'
▶ type (get):ƒ type() {\n\t\treturn "single entry";\n\t}
userRequest:'./src/index.js'
weak:false
▶ __proto__:ModuleDependency
我们看看 addEntry 做了哪些事情:
// node_modules/webpack/lib/Compilation.js
/**
*
* @param {string} context 当前编译环境的路径,一般就是项目根路径
* @param {Dependency} entry 入口文件依赖信息,经过SingleEntryPlugin.createDependency处理过的dep
* @param {string} name 入口文件名称
* @param {ModuleCallback} callback 回调(make钩子执行栈中的函数)
* @returns {void} returns
*/
addEntry(context, entry, name, callback) {
// 执行addEntry钩子上注册的任务
this.hooks.addEntry.call(entry, name);
// 声明一个入口
const slot = {
name: name,
// TODO webpack 5 remove `request`
request: null,
module: null
};
// 如果这个入口文件是一个模块依赖,关于Dependency可以查看node_modules/webpack/lib/Dependency.js
// 设置请求路径
if (entry instanceof ModuleDependency) {
slot.request = entry.request;
}
// TODO webpack 5: 当支持多个入口模块时,改为合并模块
// 如果是准备好了的入口点,那么就覆盖它,否则就添加进去(多入口打包)
const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
if (idx >= 0) {
// 覆盖现有入口点
this._preparedEntrypoints[idx] = slot;
} else {
this._preparedEntrypoints.push(slot);
}
// 通过入口文件和上下文环境添加模块链(解析模块依赖关系)
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
// 如果失败则抛出错误信息,并执行回调,返回异常
if (err) {
this.hooks.failedEntry.call(entry, name, err);
return callback(err);
}
// 如果成功返回模块则将模块绑定到入口信息slot上
if (module) {
slot.module = module;
} else {
// 否则的话从_preparedEntrypoints上移除slot
const idx = this._preparedEntrypoints.indexOf(slot);
if (idx >= 0) {
this._preparedEntrypoints.splice(idx, 1);
}
}
// 执行入口文件添加成功的钩子
this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
}
);
}
/**
*
* @param {string} context context string path
* @param {Dependency} dependency dependency used to create Module chain
* @param {OnModuleCallback} onModule function invoked on modules creation
* @param {ModuleChainCallback} callback callback for when module chain is complete
* @returns {void} will throw if dependency instance is not a valid Dependency
*/
_addModuleChain(context, dependency, onModule, callback) {
// 是否生成包含统计数据的json文件
const start = this.profile && Date.now();
const currentProfile = this.profile && {};
// 是否在webpack打包错误时立即退出
const errorAndCallback = this.bail
? err => {
callback(err);
}
: err => {
err.dependencies = [dependency];
this.errors.push(err);
callback();
};
// 如果不是标准的依赖模块就抛出错误信息
if (
typeof dependency !== "object" || dependency === null || !dependency.constructor
) {
throw new Error("Parameter 'dependency' must be a Dependency");
}
// 从依赖工厂中取出模块工厂(创建模块的工厂实例,继承至NormalModuleFactory)
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// 不存在就抛出错误信息
if (!moduleFactory) {
throw new Error(
`No dependency factory available for this dependency type: ${dependency.constructor.name}`
);
}
// this.semaphore 这个类是一个编译队列控制,对执行进行了并发控制,默认并发数为 100
// 超过后存入 semaphore.waiters,根据情况再调用 semaphore.release 去执行存入的事件 semaphore.waiters。
this.semaphore.acquire(() => {
moduleFactory.create(
{
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
},
(err, module) => {
// 如果创建模块失败,则调出编译队列中等待执行的任务执行掉,目前是入口文件打包,暂时没有待执行的
// 然后就抛出错误信息,提示没有找到入口模块
if (err) {
this.semaphore.release();
return errorAndCallback(new EntryModuleNotFoundError(err));
}
// 创建结束时间
let afterFactory;
// 如果当前存在统计stats的文件
if (currentProfile) {
// 设置统计时间
afterFactory = Date.now();
currentProfile.factory = afterFactory - start;
}
// 进行module缓存
const addModuleResult = this.addModule(module);
module = addModuleResult.module;
// 将module添加到入口文件中 entries
onModule(module);
dependency.module = module;
module.addReason(null, dependency);
// build之后添加模块依赖信息
const afterBuild = () => {
if (addModuleResult.dependencies) {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
} else {
return callback(null, module);
}
};
if (addModuleResult.issuer) {
if (currentProfile) {
module.profile = currentProfile;
}
}
if (addModuleResult.build) {
this.buildModule(module, false, null, null, err => {
if (err) {
this.semaphore.release();
return errorAndCallback(err);
}
if (currentProfile) {
const afterBuilding = Date.now();
currentProfile.building = afterBuilding - afterFactory;
}
this.semaphore.release();
afterBuild();
});
} else {
this.semaphore.release();
this.waitForBuildingFinished(module, afterBuild);
}
}
);
);
}
// node_modules/webpack/lib/NormalModuleFactory.js
// 生成factory函数,里面执行resolver钩子生成resolver函数并执行
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
// 调用resolver钩子,返回resolver函数
let resolver = this.hooks.resolver.call(null);
// Ignored
if (!resolver) return callback();
// 执行resolver函数,第一个参数是beforeResolve执行结果,进行模块解析
resolver(result, (err, data) => {
if (err) return callback(err);
// Ignored
if (!data) return callback();
// direct module
if (typeof data.source === "function") return callback(null, data);
// 如果有resolve结果,执行afterResolve上注册的钩子函数,执行完毕将结果返回给回调
this.hooks.afterResolve.callAsync(data, (err, result) => {
if (err) return callback(err);
// Ignored
if (!result) return callback();
let createdModule = this.hooks.createModule.call(result);
if (!createdModule) {
if (!result.request) {
return callback(new Error("Empty dependency (no request)"));
}
createdModule = new NormalModule(result);
}
createdModule = this.hooks.module.call(createdModule, result);
return callback(null, createdModule);
});
});
});
// 生成resolver函数
// 参数 data
// {contextInfo: {…}, resolveOptions: {…}, context: '/Users/---/lagou-edu/webpack-entry', request: './src/index.js', dependencies: Array(1)}
// context:'/Users/---/lagou-edu/webpack-entry'
// ▼ contextInfo:{issuer: '', compiler: undefined}
// compiler:undefined
// issuer:''
// ▶ __proto__:Object
// ▼ dependencies:(1) [SingleEntryDependency]
// ▼ 0:SingleEntryDependency {module: null, weak: false, optional: false, loc: {…}, request: './src/index.js', …}
// ▶ loc:{name: 'index'}
// module:null
// optional:false
// request:'./src/index.js'
// ▶ type (get):ƒ type() {\n\t\treturn "single entry";\n\t}
// userRequest:'./src/index.js'
// weak:false
// ▶ __proto__:ModuleDependency
// length:1
// ▶ __proto__:Array(0)
// request:'./src/index.js'
// ▶ resolveOptions:{}
// ▶ __proto__:Object
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
// 该函数作用为解析构建所有 module 所需要的 loaders 的绝对路径及这个 module 的相关构建信息
// 取出上下文信息,编译环境路径和请求文件路径
const contextInfo = data.contextInfo;
const context = data.context;
const request = data.request;
// loader 模块
// Resolver {_pluginCompat: SyncBailHook, fileSystem: CachedInputFileSystem, hooks: {…}, withOptions: ƒ}
// ▶ _pluginCompat:SyncBailHook {_args: Array(1), taps: Array(4), interceptors: Array(0), call: ƒ, promise: ƒ, …}
// ▶ fileSystem:CachedInputFileSystem {fileSystem: NodeJsInputFileSystem, _statStorage: Storage, _readdirStorage: Storage,_readFileStorage: Storage, _readJsonStorage: Storage, …}
// ▶ hooks:{resolveStep: SyncHook, noResolve: SyncHook, resolve: AsyncSeriesBailHook, result: AsyncSeriesHook,parsedResolve: AsyncSeriesBailHook, …}
// ▶ withOptions:options => {\n\t\t\tconst cacheEntry = childCache.get(options);\n\t\t\tif (cacheEntry !== undefined)return cacheEntry;\n\t\t\tconst mergedOptions = cachedCleverMerge(originalResolveOptions, options);\n\t\t\tconstresolver = this.get(type, mergedOptions);\n\t\t\tchildCache.set(options, resolver);\n\t\t\treturn resolver;\n\t\t}
// ▶ __proto__:
// loaderResolver 和 normalResolver 都继承了 Resolver,__proto__上拥有Resolver所有的成员方法
// 下面可以通过 normalResolver.resolve 解析模块
const loaderResolver = this.getResolver("loader");
// 普通模块
const normalResolver = this.getResolver("normal", data.resolveOptions);
// Loader 模块request path是 Loader path + module path
// 这里做一下拆分
// '/Users/***/lagou-edu/webpack-entry/node_modules/html-webpack-plugin/lib/loader.js!/Users/***/lagou-edu/webpack-entry/src/index.html'
let matchResource = undefined;
let requestWithoutMatchResource = request;
const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request);
if (matchResourceMatch) {
matchResource = matchResourceMatch[1];
if (/^\.\.?\//.test(matchResource)) {
matchResource = path.join(context, matchResource);
}
requestWithoutMatchResource = request.substr(
matchResourceMatch[0].length
);
}
// 是否忽略 preLoader 以及 normalLoader
const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
// 是否忽略 normalLoader
const noAutoLoaders =
noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
// 忽略所有的 preLoader / normalLoader / postLoader
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
// 首先解析出所需要的 loader,这种 loader 为内联的 loader
let elements = requestWithoutMatchResource
.replace(/^-?!+/, "")
.replace(/!!+/g, "!")
.split("!");
// 获取资源路径
let resource = elements.pop();
// 获取每个loader及对应的options配置(将inline loader的写法变更为module.rule的写法)
elements = elements.map(identToLoaderRequest);
// 并行运行第一个参数中的回调,将返回结果以数组的形式传递给第二个参数最终回调
// 其中第一个是加载Loader模块
// 第二个是加载普通模块
asyncLib.parallel(
[
// callback =>
// this.resolveRequestArray(
// contextInfo,
// context,
// elements,
// loaderResolver,
// callback
// ),
callback => {
if (resource === "" || resource[0] === "?") {
return callback(null, {
resource
});
}
// 普通的模块编译调用normalResolver的resolve方法
normalResolver.resolve(
contextInfo,
context,
resource,
{},
(err, resource, resourceResolveData) => {
if (err) return callback(err);
callback(null, {
resourceResolveData,
resource
});
}
);
}
],
(err, results) => {
// loader处理
...
}
);
});
/**
* 创建一个module
* @param {*} data
* {
* contextInfo: {
* issuer: "",
* compiler: this.compiler.name
* },
* context: context,
* dependencies: [dependency]
* },
* @param {*} callback
* @returns
*/
create(data, callback) {
// 取出模块依赖信息
const dependencies = data.dependencies;
// 查看是否存在缓存
const cacheEntry = dependencyCache.get(dependencies[0]);
// 存在就返回缓存
if (cacheEntry) return callback(null, cacheEntry);
const context = data.context || this.context;
const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
const request = dependencies[0].request;
const contextInfo = data.contextInfo || {};
// 执行beforeResolve钩子上注册的任务
this.hooks.beforeResolve.callAsync(
{
contextInfo,
resolveOptions,
context,
request,
dependencies
},
(err, result) => {
// 未做处理的话,result 还是传入的第一个参数 { contextInfo: ... }
if (err) return callback(err);
// Ignored
if (!result) return callback();
// 执行完毕就执行factory钩子生成工厂函数
// factory 钩子上注册的函数返回一个 factory 函数
const factory = this.hooks.factory.call(null);
// Ignored
if (!factory) return callback();
// 调用factory函数,第一个参数是beforeResolve钩子的执行结果,第二个参数是回调
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);
});
}
);
}
// node_modules/enhanced-resolve/lib/Resolver.js
/**
*
* @param {上下文信息} context
* {issuer: '', compiler: undefined}
* compiler:undefined
* issuer:''
* ▶ __proto__:Object
* @param {路径} path '/Users/---/lagou-edu/webpack-entry'
* @param {请求文件} request './src/index.js'
* @param {解析环境} resolveContext {}
* @param {回调} callback
* @returns
*/
resolve(context, path, request, resolveContext, callback) {
const obj = {
context: context,
path: path,
request: request
};
const message = "resolve '" + request + "' in '" + path + "'";
// 调用doResolve进行解析
return this.doResolve(
this.hooks.resolve,
obj,
message,
{
missing: resolveContext.missing,
stack: resolveContext.stack
},
(err, result) => {
if (!err && result) {
return callback(
null,
result.path === false ? false : result.path + (result.query || ""),
result
);
}
// 没有加载结果的话再加载一遍,打印加载日志
const localMissing = new Set();
const log = [];
return this.doResolve(
this.hooks.resolve,
obj,
message,
{
log: msg => {
if (resolveContext.log) {
resolveContext.log(msg);
}
log.push(msg);
},
missing: localMissing,
stack: resolveContext.stack
},
(err, result) => {
if (err) return callback(err);
const error = new Error("Can't " + message);
error.details = log.join("\n");
error.missing = Array.from(localMissing);
this.hooks.noResolve.call(obj, error);
return callback(error);
}
);
}
);
}
/**
*
* @param {resolve钩子} hook
* @param {请求信息(包含编译环境、路径、文件)} request
* {context: {…}, path: '/Users/---/lagou-edu/webpack-entry', request: './src/index.js'}
* ▶ context:{issuer: '', compiler: undefined}
* request:'./src/index.js'
* path:'/Users/---/lagou-edu/webpack-entry'
* ▶ __proto__:Object
* @param {请求的文字描述('resolve './src/index.js' in '/Users/---/lagou-edu/webpack-entry'')} message
* @param {解析环境} resolveContext
* {missing: undefined, stack: undefined}
* stack:undefined
* missing:undefined
* ▶ __proto__:Object
* @param {*} callback
* @returns
*/
doResolve(hook, request, message, resolveContext, callback) {
if (typeof callback !== "function")
throw new Error("callback is not a function " + Array.from(arguments));
if (!resolveContext)
throw new Error(
"resolveContext is not an object " + Array.from(arguments)
);
const stackLine = // 创建一个堆栈线 'resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js'
hook.name +
": (" +
request.path +
") " +
(request.request || "") +
(request.query || "") +
(request.directory ? " directory" : "") +
(request.module ? " module" : "");
let newStack;
if (resolveContext.stack) {
newStack = new Set(resolveContext.stack);
if (resolveContext.stack.has(stackLine)) {
// 防止递归,如果当前resolve环境中存在当前解析,就阻止运行
// 返回递归错误
const recursionError = new Error(
"Recursion in resolving\nStack:\n " +
Array.from(newStack).join("\n ")
);
recursionError.recursion = true;
if (resolveContext.log)
resolveContext.log("abort resolving because of recursion");
return callback(recursionError);
}
newStack.add(stackLine);
} else {
// 否则的话就存入新的堆栈中
newStack = new Set([stackLine]);
}
this.hooks.resolveStep.call(hook, request);
// 注册的taps 或者 拦截器 不为空
// {type: 'async', fn: ƒ, name: 'UnsafeCachePlugin'}
if (hook.isUsed()) {
const innerContext = createInnerContext(
{
log: resolveContext.log,
missing: resolveContext.missing,
stack: newStack
},
message
);
// 返回钩子的执行结果(解析结果)
return hook.callAsync(request, innerContext, (err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
callback();
});
} else {
callback();
}
}
流程:addEntry -> _addModuleChain -> create -> resolve -> doResolve
-
addEntry组装入口信息,然后交由_addModuleChain处理,顾名思义,添加模块链就是添加模块之间的相互依赖 -
_addModuleChain取出模块工厂(normalModuleFactory 或者 contextModuleFactory),调用create创建解析工厂factory -
create执行factory钩子上注册的函数normalModuleFactory生成factory函数并执行 -
factory函数里面又执行resolver钩子生成resolve函数,然后执行这个函数,这个resolver主要就是处理Loader的 -
resolve函数里面先取出加载 module 的 factory,也就是this.resolverFactory(type, resolveOptions),这个this.resolverFactory就是Compiler中挂载的resolves(Compiler.js 第 151 行),里面在resolverFactory挂载了一堆plugin;然后进行加载模块路径分析,判断是 loader 模块还是普通文件模块(Loader 模块路径通常解析成 LoaderJsPath + filePath,普通文件模块解析成 filePath);最终通过一个并行任务asyncLib.parallel并行执行 Loader 模块和普通模块的解析,最终将解析结果合并后给回调处理。 -
在普通模块的解析中直接调用
resolve方法进行解析(normalResolver.resolve) -
resolve中进行参数拼装,然后又调用doResolve进行最终解析