背景
俗话说:“授人以鱼不如授人以渔”,重要的不是解读源码,而是,手把手教会其他人如何解读源码的方法,才是真正的“授人以渔” 。所以,想着以工程化打包工具webpack为例,深度解析其中的原理和思路。和大家一起学习,实现共用学习,共同进步,共用成长。
在工作中,大家都会使用各种打包工具。目前,大家耳熟能详的有webpack,esbuild,rollup,vite等。但是大家少有机会深度了解,要不已经搭建好了相关的配置,要不有工程模版。当遇到问题相关打包问题时,翻阅官方文档快速的解决问题,成为了一个“会用就行”的配置工程师。那么,是否真的了解其中的流程和机制吗?对大部分工程师来说,工具都是一个“黑盒”,大家会用就行。但是,要拒绝平庸,从 webpack 的深度剖析开始,希望能学有所得!
导读
webpack目前已进入5.x版本时代,集成了非常非常多的能力,其中包括:代码打包,Tree-shaking,代码分割,HMR,Plugins,Loader,sourceMap等等。对应的代码量也到达恐怖的程度,阅读源码的上手成本非常非常的高。那么,我们如何上手阅读源码,从哪里看起,具体怎么看呢?
在这里,提供几个方式
-
专注主流程,针对于webpack初始化 -> 递归解析构建 -> 生成代码的主流程进行分析。当然,针对一些重要的上下文和相关知识也要进行拓展。
-
调试代码,通过VsCode打断点对webpack流程进行调试,方便理解
-
画图总结,针对于各种功能,代码结构等,进行画图总结能加深理解。在这里推荐泳道图和流程图
-
注重思想,注重代码涉及的设计模式,架构设计等核心思想,对其深入挖掘,定能有所收获
本文以上述为主旨,针对其他涉及的核心知识点会在**「知识点Tips章节」**进行补充说明 。如果有不正确的地方,欢迎反馈指正
基本知识
Entry: 指定从哪个路径开始构建其内部依赖关系图,可以是多个入口
Output: 指定bundles命名和输入路径
Loaders: 由于webpack只能理解 JavaScript 和 JSON 文件,Loader帮助webpack处理其它类型的文件并转换
Plugins: 用于webpack的拓展,能够执行更广泛的任务,例如bundle优化,资源管理,环境变量注入等
Mode: 参数值为development,production和none,用于开启每个不同的优化
Browser Compatibility: webpack支持所有es5的浏览器,低版本浏览器需要使用polyfill
Module: Webpack Module根据导入语句分析依赖关系,把代码分为不同的模块。例如:ES2015 import,commonJS require、AMD require、@import css/sass/less file,等,文件和资源都属于webpack中的module
chunk: 是指 Webpack 在打包过程中生成的一个独立的文件,由多个module代码组成。chunk通常用于实现按需加载(dynamic import)和代码分割(code splitting)等功能。将代码分割成多个较小的代码块,以优化加载性能和资源利用率。chunk有两种形式:
- initial是入口点的主要chunk。 该chunk包含您为入口点指定的所有module及其依赖
- non-initial 是一个可能被延迟加载的chunk。 当使用动态导入或SplitChunksPlugin时可能会出现
chunkGraph: 是 Webpack 中表示chunk及其之间关系的结构,用于管理chunk的创建、拆分和合并,以及优化代码块的加载和资源利用
chunkGroups: 由多个chunk组成的集合,包括入口chunkGroups、异步chunkGroups和共享chunkGroups等。用于更高效的构建和加载
前置工作
-
创建一个webpack打包的工程,可以参考webpack快速开始(webpack.js.org/guides/gett…
-
下载webpack源码(github.com/webpack/web…
-
下载webpack-cli源码(github.com/webpack/web…
-
创建一个文件夹,把上面三个工程放在一起,并下载依赖
目录结构:
webpack-source
|--myProgram // 测试工程
|--webpack // webpack源码
|--webpack-cli // webpakc-cli源码
把webpack-source项目代码提交 github 上了,链接点 这里
webpack 源码版本:v5.90.1
调试源码
首先,捋清楚思路。之后,逐步分析,各个击破
-
在
myProgram工程中,如何执行webpack打包操作? -
如何调用本地webpack源码进行打包?
-
如何调试webpack源码,剖析打包流程?
webpack打包流程
首先,按照习惯先查看package.json,打开myProgram项目根目录下的package.json文件。发现scripts中定义的build打包命令,其本质上是执行webpack
{
......
"scripts": {
"build": "webpack",
},
}
其次,如何定位webpack打包命令执行的脚本在哪里?是在webpack依赖定义的,我们找到myProgram工程根目录下的node_modules/webpack路径。照例先查看package.json文件,发现定义了bin字段。"webpack": "bin/webpack.js"表示定义了一个webpack命令,会执行对应路径的脚本进行处理(在**「知识点Tips」**章节,介绍npm bin字段)
{
......
"bin": {
"webpack": "bin/webpack.js"
}
}
然后,找到bin/webpacask.js路径,开启debugger模式打断点,跟踪调用堆栈。发现调用runCli方法,其中require路径是node_modules/webpack-cli/bin/cli.js。根据线索继续追踪,找到最后调用的打包方法
最后,绕了一大圈,发现使用npm commander包处理命令CLI。并且,webpack-cli获取webpack.config.js配置文件,执行webpack/lib/webpack.js路径下的webpack(options, callback)方法,把webpack.config配置当作options参数传入。到这里,我们找到了打包核心方法和路径,接下来想想如何调用本地webpack源码?🤔️
调用webapck源码
目前,了解到npm run build打包流程,本质上是调用webpack方法(node_modules/webpack/lib/webpack.js)。那么,自己写一个build.js脚本,导入并调用webpack方法,是不是可行?说干就干,尝试一下💪
第一步:修改myProgram工程下package.json文件定义的build命令,并创建对应build.js文件,内容如下:
{
"scripts": {
"build": "node build.js",
},
}
第二步:解析webpack/lib/webpack.js路径下webpack(options, callback)源码
const webpack = (options, callback) => {
......
if (callback) {
try {
const { compiler, watch, watchOptions } = create();
if (watch) {
......
} else {
compiler.run(() => {......});
}
return compiler;
} catch (err) {
......
}
} else {
const { compiler, watch } = create();
if (watch) {
......
}
return compiler;
}
}
webpack方法接收两个参数,options 和 callback,核心逻辑:
-
接收参数
- options:
webpack.config.js配置参数 - callback:compiler.run之后的回调方法
- options:
-
两种调用方式
- 有callback,直接调用compiler.run方法 , 最后返回compiler对象
- 无callback,直接返回compiler对象,由用户调用compiler.run方法和calback
-
核心方法,compiler.run()开始执行打包操作
第三步:在build.js文件中引入webpack(options, callback)方法和webpack.config.js配置文件,进行调用
const webpack = require('../webpack-master/lib/webpack')
const config = require('./webpack.config')
// 传入callback调用方法 webpack(config, () => {}) // 不传入callback调用方法 webpack(config).run(() => {})
在myProgram项目下,执行npm run build命令尝试进行打包。在项目根目录下,成功生成打包产物dist文件夹。同时,也证实了方法可行~
断点调试
最后一步,VsCode开启debug模式,在build.js文件中的webpack(options, callback)方法上打上断点。可以一步步调试,开始上手解读webpack源码。到这里,我们已经完成了相关的准备工作,取得了阶段性成功!🎉
Tips:关于VSCode debug调试的使用,在这里推荐文章《2022年了,该学会用VSCode debug了》
流程总结
核心主流程
主流程图
暂时无法在飞书文档外展示此内容
初始化阶段
webpack.create()
从构建方法webpack.run()入手阅读,由于这一小节的目标是初始化流程。我们发现重点是compiler.run方法,那看看create方法是如何创建compiler对象,让我们深入了解一下~
const webpack = (options, callback) => {
......
if (callback) {
try {
// compiler为核心对象,create方法并返回
const { compiler, watch, watchOptions } = create();
} catch (err) {
......
}
} else {
const { compiler, watch } = create();
......
return compiler;
}
}
const create = () => {
// 校验webpack.config.js的参数
validateSchema(webpackOptionsSchema, options);
let compiler;
// options为数组的情况较少,忽略
if (Array.isArray(options)) {
......
} else {
compiler = createCompiler(options);
......
}
return { compiler, watch, watchOptions } ;
};
webpack.create()核心逻辑:
-
validateSchema校验webpack.config.js的参数
-
调用createCompiler(options)方法创建compiler对象
-
返回compiler, watch, watchOptions
createCompiler()
继续跟踪createCompiler方法,初始化的核心处理方法都在WebpackOptionsApply()方法中,作用是把webpack配置转化为内置插件plugin进行初始化处理。这是一个值得学习的插件思维,后面小节会深入了解~
const createCompiler = rawOptions => {
// 对webpack.config进行规范化和默认值操作
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
// 创建compiler实例
const compiler = new Compiler(options.context);
// 把node环境变量绑定到compiler实例
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
// 遍历&注册所有自定义plugin
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 核心方法,把webpack配置使用内置插件plugin进行初始化处理
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
createCompiler核心流程:
-
调用getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults对webpack.config对options进行规范化和默认值操作
-
new compiler(),初始化compiler实例(在**「知识点Tips」**章节,介绍Compiler类)
-
new NodeEnvironmentPlugin调用内置插件,把node环境变量绑定到compiler实例
-
遍历options.plugins,注册所有自定义plugin
-
调用environment、afterEnvironment生命周期回调方法
-
new WebpackOptionsApply().process(options, compiler),进行各种options解析
-
调用initialize生命周期回调方法,说明初始化已经执行完成
-
返回compiler实例
WebpackOptionsApply()
下面我们来了解一下WebpackOptionsApply().process(options,compiler)方法,其主要作用是解析webpack.config.js配置,数十个插件在该方法中完成注册,在合适的时机运行plugin插件,注册&运行依赖于npm tapable。同时,plugins插件和hooks贯穿了webpack全文,是一个重要概念,让我们继续往下看~
class WebpackOptionsApply extends OptionsApply {
......
process(options, compiler) {
......
if (options.externals) {
const ExternalsPlugin = require("./ExternalsPlugin");
// 解析options.xxx配置,注册插件进行处理
new ExternalsPlugin(options.externalsType, options.externals).apply(
compiler
);
}
// 注册插件进行初始化处理
new EntryOptionPlugin().apply(compiler);
// hooks.<hook name>.call调用,plugin插件响应
compiler.hooks.entryOption.call(options.context, options.entry);
new RuntimePlugin().apply(compiler);
......
}
}
WebpackOptionsApply()核心流程:
-
new xxxPlugin().apply(compiler)的写法注册内置插件,用以解析webpack.config.js配置,同时传入compiler实例 -
对入口、运行时等进行处理,例如:
new EntryOptionPlugin().apply(compiler) -
调用
npm ``tapable方法hooks.<hook name>.call,plugin插件响应(在**「知识点Tips」章节**,介绍tapable)
到这里,完成初始化阶段,下一步探索解析构建流程🎉🎉🎉
流程总结
解析构建
Compiler.run
回过头来,继续回到Compiler.run()方法,开始探索解析构建的任务线
const webpack = (options, callback) => {
......
if (callback) {
try {
const { compiler, watch, watchOptions } = create();
......
compiler.run(() => {......});
return compiler;
}
}
}
run(callback) {
......
// 失败回调
const finalCallback = (err, stats) => {};
// compile回调函数
const onCompiled = (err, compilation) => {};
// 当前作用域run方法
const run = () => {
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
// 调用readRecords方法,json文件形式记录模块的build变化
this.readRecords(err => {
if (err) return finalCallback(err);
// 执行编译
this.compile(onCompiled) ;
});
});
});
};
if (this.idle) {
......
} else {
run() ;
}
}
Compiler.run()核心逻辑:
- 定义finalCallback失败回调方法,用于err处理
- 定义onCompiled方法,传入this.compile用于回调
- 执行当前作用域run方法
- 调用readRecords方法,json文件形式记录模块的build变化
- Compiler.compile(onCompiled),执行编译
Compiler.compile
对核心方法 Compiler.compile(onCompiled)继续跟踪,穷追不舍~
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
......
this.hooks.compile.call(params);
// 创建Compilation实例
const compilation = this.newCompilation(params);
// 传入Compilation实例,调用插件进行构建
this.hooks.make.callAsync(compilation, err => {
......
this.hooks.finishMake.callAsync(compilation, err => {
......
process.nextTick(() => {
// 对mudule上的错误进行处理
compilation.finish(err => {
......
// 对打包产物封装
compilation.seal(err => {
......
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
});
});
});
});
});
});
}
Compiler.compile()核心流程:
- newCompilationParams方法,初始化Compilation参数
- 执行hooks生命周期beforeCompile -> compile -> make -> finishMake -> afterCompile, 可以看出是整个编译的流程,从编译前 -> 编译后
- newCompilation方法,创建Compilation实例(在**「知识点Tips」章节**,介绍Compilation类)
- hooks.make,传入Compilation实例,调用插件进行构建
- process.nextTick方法在下一个事件循环迭代之前执行该任务,compilation.finish方法,对mudule上的错误进行处理(在**「知识点Tips」章节**,介绍process.nextTick)
- compilation.seal方法,对打包产物封装
- 构建 & 封装流程结束之后,调用回调(onCompiled)输出
hooks.make
目前,已知hooks.make注册插件会处理编译,但怎么定位到是具体哪一个插件?如何去顺藤摸瓜找到最后的真相?靠直觉,还是靠有理有据的连蒙带猜,让我们一起尝试找找~
第一步:全局搜索
compiler.hooks.make.callAsync是tapable的调用事件,那我们全局搜索hooks.make看看哪些插件注册监听了当前hooks,尝试寻找线索(在**「知识点Tips」章节**,介绍tapable)
通过全局搜索找到了7个相关插件进行注册,根据插件名猜测,圈定缩小范围,优先浏览EntryPlugin和ContainerPlugin
第二步:顺藤摸瓜
EntryPlugin中的注册插件方法,根据入口顺藤摸瓜找到核心流程,一步步深入观察。从EntryPlugin -> compilation.addEntry -> compilation._addEntryItem -> compilation.addModuleTree -> handleModuleCreation。最后,找到handleModuleCreation方法处理和创建模块。看到这里,离真相越来越近了~
// EntryPlugin注册插件,监听make hooks
apply(compiler) {
......
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
}
// compilation.addEntry方法,判断options并解析
addEntry(context, entry, optionsOrName, callback) {
......
this._addEntryItem(context, entry, "dependencies", options, callback);
}
// compilation._addEntryItem方法,执行addModuleTree把当前modules添加到module tree中
_addEntryItem(context, entry, target, options, callback) {
......
this.addModuleTree({......}, (err, module) => {});
}
// compilation.addModuleTree方法,创建moduleFactory,执行handleModuleCreation
addModuleTree({ context, dependency, contextInfo }, callback) {
......
const moduleFactory = this.dependencyFactories.get(Dep);
if (!moduleFactory) {
return callback();
}
this.handleModuleCreation({......},err => {......});
}
核心流程:
- EntryPlugin,createDependency解析入口依赖,调用addEntry添加入口
- addEntry,判断options并解析,调用_addEntryItem
- _addEntryItem,执行hooks生命周期addEntry -> failedEntry -> succeedEntry,执行addModuleTree把当前modules添加到module tree中
- addModuleTree , 创建moduleFactory,执行handleModuleCreation
第三步:找到线索
到这里,顺着流程一步步找到了handleModuleCreation方法处理module,感觉找到了线索。大胆猜测一下,下一步应该就是对module的解析和构建。接着向下看handleModuleCreation源码
handleModuleCreation(
{ ...... },
callback
) {
// module的收集操作
const moduleGraph = this.moduleGraph;
......
// 创建并返回newModule
this.factorizeModule(
{......},
(err, newModule) => {
if (err) { ...... }
// 添加模块
this.addModule(newModule, (err, module) => {
if (err) { ...... }
......
this._handleModuleBuildAndDependencies(......);
});
}
);
}
_handleModuleBuildAndDependencies(params) {
......
this.buildModule(module, err => {......});
}
_buildModule(module, callback) {
......
module.needBuild(
{ ...... },
(err, needBuild) => {
if (err) return callback(err);
......
// 模块构建
module.build(......);
}
);
}
handleModuleCreation核心流程:
- 创建moduleGraph,在当前方法中进行module的收集操作
- 执行factorizeModule,将传入的参数加到队列 factorizeQueue,并创建并返回newModule,以参数的形式传递给回调方法
- 执行addModule传入newModule参数,回调中执行processModuleDependencies -> buildModule方法
- 把newModule加入队列buildQueue,执行_buildModule方法处理(在**「知识点Tips」章节**,介绍AsyncQueue异步队列)
- module.needBuild判断module是否需要build,如果需要执行callback回调
- module.build对模块构建build操作
第四步:最后真相
最后的最后,终于找到了真相!module.build()就是模块构建方法,但是,发现进入module.build竟然只有一个抛出AbstractMethodError错误的操作。难道,是思路错了,方向不对?别怕,我们会断点调试,在module.build方法上打个端点,深入看看~
build(options, compilation, resolver, fs, callback) {
const AbstractMethodError = require("./AbstractMethodError");
throw new AbstractMethodError();
}
在执行build方法callback打断点调试,逐步执行。对module.build添加debug监听,发现展示FuntionLocation路径是webpack/lib/NormalModule.js。深入探究发现了其中的秘密:NormalModule继承了Modules类,定义了同名方法build,对build进行了override重写。(在**「知识点Tips」章节**,介绍继承重写)
所以,最后实际调用并不是Module.build,而是NormalModule.build,最后的真相浮出水面~
NormalModule.build
我们来看看override重写实现NormalModule.build方法源码~
build(options, compilation, resolver, fs, callback) {
......
// 核心方法_doBuild
return this._doBuild(options, compilation, resolver, fs, err => {
......
const handleParseError = e => { ...... };
const handleParseResult = result => { ...... };
const handleBuildDone = () => { ...... };
if (this.shouldPreventParsing(noParseRule, this.request)) {
......
return handleBuildDone();
}
let result;
try {
// 对ast或源码进行解析
result = this.parser.parse(this._ast || this._source.source(), {......});
} catch (e) {
// 处理解析错误
handleParseError(e);
return;
}
// 处理解析结果
handleParseResult(result);
});
}
NormalModule.build核心逻辑:
- 初始化一些相关属性,例如,buildMeta,buildInfo等。在当前方法中进行消费
- 调用
NormalModule._doBuild方法,定义相关callback回调 - 回调中定义handleParseError、handleParseResult和handleBuildDone相关方法
NormalModule.parser.parse,对ast或源码进行解析- 在合适的时机进行调用定义的handle处理方法
NormalModule._doBuild
进入NormalModule._doBuild核心方法,看看其中源码~
_doBuild(options, compilation, resolver, fs, callback) {
const processResult = (err, result) => {......};
// 执行loader转化模块
runLoaders({ ...... },
(err, result) => {
......
processResult(err, result.result);
}
);
}
NormalModule._doBuild核心流程:
- runLoaders(loader-runner)用于执行loader,对
webpack.config.js loader配置正则匹配到的文件进行转化(在**「知识点Tips」章节**,介绍loader-runner) - 在回调方法中,执行processResult处理转化后的result结果
NormalModule.parser.parse
回头来看NormalModule.parser.parse核心方法,用于解析AST和源码。这个方法和上述module.build方法一样,NormalModule.parser继承了Parse类型,子类override重写。和上面的步骤一样,找到真正的实现方法的路径是webpack/lib/javascript/JavascriptParser.js,源码如下:
parse(source, state) {
......
if (typeof source === "object") {
......
} else {
comments = [];
// 解析源码生成AST
ast = JavascriptParser._parse(source, {
sourceType: this.sourceType,
onComment: comments,
onInsertedSemicolon: pos => semicolons.add(pos)
});
}
......
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);
}
}
NormalModule.parser.parse核心流程:
- JavascriptParser._parse解析源码生成AST
- 调用detectMode、preWalkStatements、blockPreWalkStatements、walkStatements方法进行处理
Compilation.processModuleDependencies
上述完成一个模块解析,那如何递归操作解析所有依赖关系?从头来看,一起回溯到**hooks.make ->「找到线索」小节**对handleModuleCreation方法解析,其中调用Compilation.buildModule方法callback回调。执行Compilation.processModuleDependencies方法把modules添加队列中,同时会触发_processModuleDependencies方法进行处理。对dependencies依赖进行遍历执行handleModuleCreation方法,直到所有module模块解析构建完成~
// webpack/lib/Compilation.js
handleModuleCreation({......}, callback) {
......
this.factorizeModule({......}, (err, newModule) => {
......
this.addModule(newModule, (err, module) => {
......
this._handleModuleBuildAndDependencies(......);
}
}
}
_handleModuleBuildAndDependencies(params) {
......
this.buildModule(module, err => {
......
// 触发_processModuleDependencies方法进行处理
this.processModuleDependencies(module, callback);
});
}
processModuleDependencies(module, callback) {
// new AsyncQueue类型,执行_processModuleDependencies进行处理和响应
this.processDependenciesQueue.add(module, callback);
}
_processModuleDependencies(module, callback) {
......
for (const item of sortedDependencies) {
inProgressTransitive++;
this.handleModuleCreation(item, err => { ...... });
}
}
processModuleDependencies核心流程:
- Compilation.buildModule方法callback回调中执行processModuleDependencies方法
- 添加module到_processModuleDependencies(AsyncQueue类型)中,执行_processModuleDependencies进行处理和响应(在**「知识点Tips」章节**,介绍AsyncQueue异步队列)
- 遍历dependencies依赖,递归执行handleModuleCreation方法,直到所有module模块解析构建完成
遍历模块Modules
这个小节,我们看看如何遍历模块modules,并对依赖dependencies进行解析的~
_processModuleDependencies(module, callback) {
......
// 遍历解析sortedDependencies数组,递归执行handleModuleCreation
const onDependenciesSorted = err => {
......
for (const item of sortedDependencies) {
inProgressTransitive++;
// 递归执行handleModuleCreation方法
this.handleModuleCreation(item, err => {
......
if (--inProgressTransitive === 0) onTransitiveTasksFinished();
});
}
// 遍历结束
if (--inProgressTransitive === 0) onTransitiveTasksFinished();
};
// 遍历任务结束,AsyncQueue异步队列并行队列数量减一
const onTransitiveTasksFinished = err => {
......
};
// 判断是否开启options.module.unsafeCache,开启则从缓存中获取moudle解析结果
// 最后,执行processDependencyForResolving
const processDependency = (dep, index) => {
......
processDependencyForResolving(dep);
};
// 优化依赖项处理,记录依赖结果到缓存中;同时,收集sortedDependencies用于后续遍历消费
// 对比方法:
// 快速对比path1: 与前一个项具有相同的构造函数constructor
// 快速对比path2: 与前一个项具有相同的factory
const processDependencyForResolving = dep => {
......
};
// 首次遍历,解析module队列中的依赖dependencies
try {
const queue = [module];
do {
const block = queue.pop();
if (block.dependencies) {
currentBlock = block;
let i = 0;
// 解析依赖dependencies
for (const dep of block.dependencies) processDependency(dep, i++) ;
}
if (block.blocks) {
// 遍历blocks,获取其中modules添加入modules队列中
for (const b of block.blocks) queue.push(b);
}
} while (queue.length !== 0);
} catch (e) {
return callback(e);
}
// 遍历sortedDependencies数组,递归执行handleModuleCreation
if (--inProgressSorting === 0) onDependenciesSorted();
}
遍历模块Modules核心流程:
-
首次操作,遍历blocks属性收集模块modules添加到队列中。同时,遍历获取dependencies依赖项并进行解析
-
遍历过程中调用
processDependency方法,在其中判断是否开启options.module.unsafeCache,开启则从缓存中获取moudle解析结果。最后,执行processDependencyForResolving -
执行
processDependencyForResolving方法记录依赖结果缓存和收集sortedDependencies数组 -
modules队列遍历结束后执行
onDependenciesSorted方法,遍历sortedDependencies数组,结束执行onTransitiveTasksFinished方法 -
递归执行
handleModuleCreation方法,直到所有module模块解析构建完成
到这里,通过遍历操作完成所有module模块的解析,下一步是生产输出~
流程总结
生成输出
上面章节,介绍了解析构建的全流程,接下来我们介绍最后一步「生成输出」的过程。hooks.make注册插件处理完毕,回到Compiler.compile -> hooks.make.callAsync 的回调callback中继续执行。
callback(hooks.make)
执行顺序hooks.make -> callback(make) -> hooks.finishMake -> callback(finishMak) -> process.nextTick -> compilation.finish -> callback(compilation.finish) -> compilation.seal -> hooks.afterCompile回调嵌回调。其中,compilation.seal方法是核心封装方法
compile(callback) {
......
this.hooks.beforeCompile.callAsync(params, err => {
......
this.hooks.make.callAsync(compilation, err => {
......
this.hooks.finishMake.callAsync(compilation, err => {
......
process.nextTick(() => {
// 遍历modules对其中dependencies依赖error和warning进行report处理
compilation.finish(err => {
......
// 进行封装
compilation.seal(err => {
......
this.hooks.afterCompile.callAsync(compilation, err => {
.......
});
});
});
});
});
});
});
}
callback(hooks.make) 核心流程:
- hooks.make执行callback回调,在callback中调用hooks.finishMake
- hooks.finishMake完成,在callback中调用compilation.finish
- compilation.finish执行hooks.finishModules,遍历modules对其中dependencies依赖error和warning进行report处理
- 执行compilation.seal方法,进行封装
Tips:避免重复,参考「hooks.make」小节流程图
compilation.seal
执行各种hooks.optimizeXXXX优化hooks,注册插件响应,对modules,chunks,tree等进行处理
seal(callback) {
// 生成chunk之间的关系
const chunkGraph = new ChunkGraph(this.moduleGraph);
this.chunkGraph = chunkGraph;
// 相关插件进行响应,优化tree
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
this.hooks.afterOptimizeTree.call(this.chunks, this.modules);
// 调用各种hooks.optimizeXXXX, 注册插件响应执行进行优化操作
this.hooks.optimizeChunkModules.callAsync(
this.chunks,
this.modules,
err => {
this.codeGeneration(err => {
this._runCodeGenerationJobs(codeGenerationJobs, err => {
// 遍历module生成ModuleAssets,调用Compilation.emitAsset
this.createModuleAssets();
if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
// 获取manifest并遍历生成ChunkAssets,调用Compilation.emitAsset
this.createChunkAssets(err => { ...... });
} else {
......
}
});
});
}
);
});
}
compilation.seal核心流程:
- 对modules,chunk进行遍历解析,生成之间的关系并赋值moduleGraph、chunkGraph
- 调用各种优化hooks,注册插件响应执行进行优化操作
- 执行codeGeneration、_runCodeGenerationJobs生成相关代码code
- 执行createModuleAssets,遍历module生成ModuleAssets,调用Compilation.emitAsset
- 执行createChunkAssets,获取manifest并遍历生成ChunkAssets,调用Compilation.emitAsset
暂时无法在飞书文档外展示此内容
Compilation.emitAsset
emitAsset方法中,把相关的source源码添加到Compilation.assets属性
emitAsset(file, source, assetInfo = {}) {
if (this.assets[file]) {
if (!isSourceEqual(this.assets[file], source)) {
......
this.assets[file] = source;
this._setAssetInfo(file, assetInfo);
return;
}
......
return;
}
// 代码保存到compilation.assets
this.assets[file] = source ;
this._setAssetInfo(file, assetInfo, undefined);
}
Compiler.emitRecords
源码添加到Compilation.assets之后,如何定位到消费Compilation.assets的位置?webpack源码中,传入callback回调的写法比比皆是,导致调用堆栈非常的多。
既然,无法debug模式继续跟踪,那从头开始执行,结合当前流程一步步定位目标。最后,定位到compiler.run方法中执行了Compiler.compile(onCompiled),并且在onCompiled中执行了emitRecords方法,进行写入文件操作~
run(callback) {
const onCompiled = (err, compilation) => {
process.nextTick(() => {
// 此处为Compiler.emitAssets方法
this.emitAssets(compilation, err => {
// 输出打包产物
this.emitRecords(err => {
this.hooks.done.callAsync(stats, err => {......});
});
});
});
};
const run = () => {
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.readRecords(err => {
// 传入onCompiled作为回调方法执行
this.compile(onCompiled);
});
});
});
};
if (this.idle) {
......
} else {
run();
}
}
emitRecords(callback) {
// 把打包的产物写入指定文件夹
const writeFile = () => {
this.outputFileSystem.writeFile(......);
};
mkdirp(this.outputFileSystem, recordsOutputPathDirectory, err => {
if (err) return callback(err);
writeFile();
});
}
emitRecords核心流程:
- compiler.run方法中执行了
Compiler.compile(onCompiled),onCompiled作为callback传入 - onCompiled中执行
Compiler.emitRecords,调用文件写操作 - 最后writeFile把打包的产物写入指定文件夹,默认是dist文件夹
流程总结
暂时无法在飞书文档外展示此内容
知识点Tips
npm bin字段
当使用 npm 或者 yarn 命令安装包时,如果该包的 package.json 文件有 bin 字段,就会在 node_modules/.bin路径下复制了 bin 字段链接的脚本文件。执行脚本文件,响应定义的命令
Compiler类
从源码中可以看出,Compiler中记录了hooks生命周期,webpack相关属性,方法。没错,compiler实例从webpack流程开始创建,且只初始化一次,仅仅只有一个。记录着webpack运行环境和配置的所有属性。
class Compiler {
constructor(context) {
// hooks生命周期定义
this.hooks = Object.freeze({
......
initialize: new SyncHook([]),
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
afterDone: new SyncHook(["stats"]),
});
// 属性定义
this.webpack = webpack;
this.name = undefined;
......
}
// 相关方法定义
getCache(name){}
getInfrastructureLogger(name){}
......
}
Compiler中的hooks生命周期类型属于tapable,hooks.<hook name>.call进行调用hooks,插件的注册和执行来自于对hooks的监听。可以看出webpack插件和生命周期依赖于tapable。为了更好的理解其中的机制,一起学习一下tapable~
Compilation类
class Compilation {
constructor(compiler, params) {
// 所有模块及其依赖项属性
this.moduleGraph = new ModuleGraph();
this.chunkGraph = undefined;
this.chunks = new Set();
this.chunkGroups = [];
}
// 定义相关方法
handleModuleCreatio(){}
addModuleTre(){}
emitAsset(){}
createHash(){}
......
}
由Compiler用于创建新的编译(或构建)。Compilation实例可以访问所有模块及其依赖项(其中大部分是循环引用)。它是应用程序dependency graph中所有模块进行直接编译输出代码。在编译阶段,模块被加载、封装、优化、chunk、hash和恢复。
Tapable
介绍
一句话总结:tapack本质上是发布订阅模式,webpack中用于定义hooks和注册执行插件plugins
用法
第一步,创建一个hooks,以SyncHook为例。SyncHook是一个类,初始化传入['arg1', 'arg2']表示回调方法中存在两个参数。例如:
const { SyncHook } = require('tapable');
const myHook = new SyncHook(['arg1', 'arg2']);
第二步,注册插件,使用tap方法注册插件到hooks中,每个插件都会被添加到hooks的执行队列中,按照添加的顺序执行。定义插件名称,回调方法。例如:
myHook.tap('Plugin1', (arg1, arg2) => {
console.log('Plugin1', arg1, arg2);
});
myHook.tap('Plugin2', (arg1, arg2) => {
console.log('Plugin2', arg1, arg2);
});
myHook.tap('Plugin3', (arg1, arg2) => {
console.log('Plugin3', arg1, arg2);
});
第三步,触发hooks,使用call()方法触发hooks执行,传入两个参数。例如:
myHook.call('Hello', 'World');
类型
hooks类型
- Basic hook:只是按顺序调用每个触发的函数。
- Waterfall hook:会按顺序调用每个触发的函数。与基本hook不同的是,它会将每个函数的返回值传递给下一个函数。
- Bail hook:允许提前退出。当任何一个触发的函数返回任何值时,hook将停止执行剩余的函数。
- Loop hook:当插件返回一个非undefined值时,hook将从第一个插件重新启动。它将循环执行,直到所有插件返回undefined。
执行方式
- Sync:只能通过同步函数来触发(使用myHook.tap())。
- AsyncSeries:可以通过同步、基于回调的和基于Promise的函数来触发(使用myHook.tap()、myHook.tapAsync()和myHook.tapPromise())。它们按顺序调用每个异步方法。
- AsyncParallel:可以通过同步、基于回调的和基于Promise的函数来触发(使用myHook.tap()、myHook.tapAsync()和myHook.tapPromise())。但是,它们并行运行每个异步方法。
总结
tapable是webpack的核心模块之一,实现模块打包过程中的plugin管理,也是由webpack团队进行维护。tapable提供了定义hooks的能力,开发者可以编写plugin,监听执行。在合适的时机执行plugin,webpack利用tapable构建了一个灵活的插件系统,实现定制化行为的能力。
process.nextTick
process.nextTick 是一个 Node.js 中的函数,用于在当前执行阶段结束后立即执行回调函数。它提供了一种在事件循环(event loop)中插入任务的方式,process.nextTick 允许将回调函数插入到事件循环的当前阶段的末尾,以便在下一个时间循环迭代之前执行。使用 process.nextTick 的语法如下:
process.nextTick(callback[, ...args])
process.nextTick 的一些特点和用途:
- 立即执行:
process.nextTick中的回调函数会在当前代码执行完成后立即执行,而不需要等待其他事件循环阶段。 - 优先级高:
process.nextTick中的回调函数的优先级比setTimeout和setImmediate更高,它们会在下一个事件循环迭代之前执行。 - 递归调用:如果在
process.nextTick回调函数中再次调用process.nextTick,它们会被递归地执行,直到达到最大递归深度。 - 避免阻塞:使用
process.nextTick可以将一些需要立即执行的回调函数插入到事件循环中,以避免长时间等待其他任务的完成。
继承重写
在 JavaScript 中,子类的继承重写demo如下:
class Parent {
sayHello() {
console.log("Hello from parent");
}
}
class Child extends Parent {
sayHello() {
console.log("Hello from child");
}
}
const child = new Child();
child.sayHello(); // 输出: Hello from child
Child 类通过 extends 关键字继承了Parent类。然后,Child 类中重写了父类的 sayHello 方法,提供了自己的实现。并且,可以多个子类进行继承重写操作
另外,分享一个定位子类具体实现的方法。打断点调试,查看执行链接
loader-runner
loader-runner 的作用是模拟 webpack 在构建过程中运行 loaders 的功能。loader-runner 模块提供了一个 runLoaders 函数,它接收 loader 请求和callback回调作为参数。每个 loader 请求包含了要转换的模块代码、模块的文件路径等信息。回调函数将在 loaders 执行完成后被调用,返回经过 loaders 处理后的模块代码。使用demo如下:
const { runLoaders } = require('loader-runner');
const resource = '/path/to/resource.js';
const loaders = [
{
loader: '/path/to/loader1.js',
options: {
// Loader1 options
}
},
{
loader: '/path/to/loader2.js',
options: {
// Loader2 options
}
},
// ...
];
const context = {
resourcePath: resource,
resourceQuery: '',
emitWarning: (warning) => console.warn(warning),
emitError: (error) => console.error(error),
// 其他上下文信息
};
runLoaders({
resource,
loaders,
context,
readResource: fs.readFile.bind(fs) // 读取资源的方法
}, (err, result) => {
if (err) {
console.error(err);
} else {
const transformedSource = result.result[0].source;
}
});
在上述示例中,我们使用 loader-runner 模块的 runLoaders 函数来运行一组 loaders。我们提供了要转换的资源路径、loaders 数组、上下文信息和读取资源的方法。在回调函数中,我们可以获取经过 loaders 处理后的结果,并进行进一步的处理。
AsyncQueue异步队列
AsyncQueue异步队列是Webpack中一个任务调度器,控制异步任务的并发执行。下面是相关源码:
class AsyncQueue {
constructor({ name, parallelism, parent, processor, getKey }) {
// 属性
this._name = name;
this._parallelism = parallelism || 1;
this._processor = processor;
......
// 定义hooks生命周期
this.hooks = {
beforeAdd: new AsyncSeriesHook(["item"]),
added: new SyncHook(["item"]),
beforeStart: new AsyncSeriesHook(["item"]),
started: new SyncHook(["item"]),
result: new SyncHook(["item", "error", "result"])
};
}
// 核心方法
add(item, callback) {}
invalidate(item) {}
waitFor(item, callback) {}
stop() {}
......
}
是初始化队列queue,调用add方法添加item到队列中,定义的processor方法进行响应处理。下面是使用示例:
const queue = new AsyncQueue({
name: "xxxx", // 队列名称
parallelism: 10, // 并行处理数量
processor:() => {}, // 处理方法
parent: 'parent', // parent,其优先级高于该队列并具有共享并行性
getKey: () => {} // 每项队列的额外名称
});
// 添加item到队列中
queue.add(item)