初始化阶段
webpack.js
- 归一化 options,将部分配置转换成 webpack 需要的格式
- 创建context上下文,取的是process.cwd()
- 创建compiler实例
- 初始化流插件
- 初始化用户配置的插件,注册插件钩子
- 进一步优化options,给一些配置赋上默认值
- 初始化webpack内部插件,例如js解析器、缓存插件、添加入口的插件等。
const createCompiler = rawOptions => {
// 归一化 options,将部分配置转换成 webpack 需要的格式
const options = getNormalizedWebpackOptions(rawOptions);
// 创建context上下文,取的是process.cwd()
applyWebpackOptionsBaseDefaults(options);
// 创建 complier 的实例
const compiler = new Compiler(options.context, options);
// 初始化流插件
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 初始化用户配置的插件,注册插件钩子
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
// 进一步优化options,给一些配置赋上默认值
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 初始化webpack内部插件,例如js解析器、缓存插件、添加入口的插件等。
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
Complier.js
hooks 调用了tapable这个库!这个库是webpack团队为了webpack专门写的一个事件/钩子库。
this.hooks = Object.freeze({
/** @type {SyncHook<[]>} */
initialize: new SyncHook([]),
})
webpack是一个事件驱动的框架,webpack负责搭架子,安排好每个流程,再由事件触发插件去做具体的事情。
webpack的流程至少有以下这几个事件钩子:
- env
- init
- run
- beforeCompile
- compile
- compilation
- make:
- finishMake
- afterCompile
- emit
核心事件
1、make,读取入口文件,分析和收集依赖
EntryPlugin.js
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
// 入口插件,创建编译入口
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
2、make - finishMake 之间
EntryPlugin 的 addEntry 函数就是 make 阶段最重要的事情之一
Complition.js
// 存入 this.entryies, 后续构建 chunk 遍历的是该 map 对象
this._addEntryItem(context, entry, "dependencies", options, callback);
// 开始创建模块实例
this.handleModuleCreation(
{
factory: moduleFactory,
dependencies: [dependency],
originModule: null,
contextInfo,
context
},
(err, result) => {
if (err && this.bail) {
callback(err);
this.buildQueue.stop();
this.rebuildQueue.stop();
this.processDependenciesQueue.stop();
this.factorizeQueue.stop();
} else if (!err && result) {
callback(null, result);
} else {
callback();
}
}
);
// 异步的任务队列, 有任务就会自动执行
Compilation.prototype.factorizeModule = /** @type {{
(options: FactorizeModuleOptions & { factoryResult?: false }, callback: ModuleCallback): void;
(options: FactorizeModuleOptions & { factoryResult: true }, callback: ModuleFactoryResultCallback): void;
}} */ (
function (options, callback) {
// factorizeQueue 是一个异步的任务队列,任务队列发现有任务就会自动执行
this.factorizeQueue.add(options, callback);
}
);
buildModule(module, callback) {
// 创建loader上下文
// runLoaders,通过enhanced-resolve解析得到的模块和loader的路径获取函数,执行loader
// 调用JavascriptParser.js将loader执行完的源码解析成ast(使用了acorn工具),这步会生成当前模块的以来集合
// 生成模块的hash
// 缓存解析完的module至_modulesCache,此时已经有_source(解析后的源码)
this.buildQueue.add(module, callback);
}
构建过程
1、初始化阶段:根据配置中的 entry 找出所有的入口文件,调用 compilition.addEntry 将入口文件转换为 dependence 对象
2、构建阶段:根据 entry 对应的 dependence 创建 module 对象,调用 loader 将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理「完成模块编译」:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 「依赖关系图」
3、生成阶段:「输出资源(seal)」:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会「写入文件系统(emitAssets)」:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
构建阶段
1、调用 handleModuleCreate ,根据文件类型构建 module 子类
2、调用 loader-runner 仓库的 runLoaders 转译 module 内容,通常是从各类资源类型转译为 JavaScript 文本
3、调用 acorn 将 JS 文本解析为 AST
4、遍历 AST,触发各种钩子在 HarmonyExportDependencyParserPlugin 插件监听 exportImportSpecifier 钩子,解读 JS 文本对应的资源依赖调用 module 对象的 addDependency 将依赖对象加入到 module 依赖列表中
5、AST 遍历完毕后,调用 module.handleParseResult 处理模块依赖
6、对于 module 新增的依赖,调用 handleModuleCreate ,控制流回到第一步
7、所有依赖都解析完毕后,构建阶段结束
这个过程中数据流 module => ast => dependences => module ,先转 AST 再从 AST 找依赖。这就要求 loaders 处理完的最后结果必须是可以被 acorn 处理的标准 JavaScript 语法,比如说对于图片,需要从图像二进制转换成类似于 export default “data:image/png;base64,xxx” 这类 base64 格式或者 export default “http://xxx” 这类 url 格式。
compilation 按这个流程递归处理,逐步解析出每个模块的内容以及 module 依赖关系,后续就可以根据这些内容打包输出。
ast 树的生成与依赖收集过程
生成阶段
生成阶段则围绕 chunks 展开。经过构建阶段之后,webpack 得到足够的模块内容与模块关系信息,接下来开始生成最终资源了
1、生成 module 和 chunks 的集合对象(有很长一段代码在处理这块的逻辑)
2、调用 createModuleHashes 生成 module hash
3、调用 codeGneration 生成代码,写入缓存
4、触发 seal 回调,控制流回到 compiler 对象
5、写入文件
这一步的关键逻辑是将 module 按规则组织成 chunks ,webpack 内置的 chunk 封装规则比较简单:
1、entry 及 entry 触达到的模块,组合成一个 chunk
2、使用动态引入语句引入的模块,各自组合成一个 chunk
chunk 是输出的基本单位,默认情况下这些 chunks 与最终输出的资源一一对应,那按上面的规则大致上可以推导出一个 entry 会对应打包出一个资源,而通过动态引入语句引入的模块,也对应会打包出相应的资源。
seal 结束之后,紧接着调用 compiler.emitAssets 函数,函数内部调用 compiler.outputFileSystem.writeFile 方法将 assets 集合写入文件系统
大致上可以梳理成这么4个步骤:
- 遍历 compilation.modules ,记录下模块与 chunk 关系
- 触发各种模块优化钩子,这一步优化的主要是模块依赖关系
- 遍历 module 构建 chunk 集合
- 触发各种优化钩子