webpack工作流程
- 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
- 用上一步得到的参数初始化 Compiler 对象
- 加载所有配置的插件
- 执行对象的 run 方法开始执行编译
- 根据配置中的
entry
找出入口文件 - 从入口文件出发,调用所有配置的
Loader
对模块进行编译 - 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
- 再把每个 Chunk 转换成一个单独的文件加入到输出列表
- 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
一、初始化参数
-
从配置文件和 Shell 语句中读取与合并参数,得出最终的参数,优先级遵循"离用户越近,优先级配置越高"
- 例如同一个参数,如果配置环境中和shell语句都有同一个配置,从命令行终端执行的时候,以shell语句中的配置为住
-
安装webpack-cli脚手架
-
npm install webpack webpack-cli -D
-
如果直接使用webpack的核心模块,则不需要安装webpack-cli
-
过程实际上就是用上一步得到的参数初始化Compiler对象,并加载所有配置的插件,通过执行Compiler对象的run方法开始执行编译过程
-
-
运行webpack-cli
-
npx webpack-cli <command> <arguments>
-
过程实际上是调用webpack-cli的主入口文件bin/webpack.js。在执行时,会将命令行参数传递给webpack()函数,这个函数是webpack的主要入口点。然后,webpack内部会解析这些参数并按照webpack的配置来执行编译过程。而entry 属性用于指定入口文件或入口文件的数组。这些入口文件是 webpack 编译的起点,它会根据配置中的entry找出所有的入口文件
-
shell语句
- shell 脚本参数一般是在终端中输入或者在
package.json
的script
中配置对应的命令
参数解析
- 为了整合 webpack.config.js 中的配置和命令行参数,Webpack 使用了一个叫做 optimist 的库。optimist 是一个用于解析命令行参数的库,可以将命令行参数解析为 JavaScript 对象,这样它们就可以与 webpack.config.js 中的配置进行合并。它可以帮助 Webpack 将用户通过命令行传递的参数与配置文件中的选项整合在一起,形成一个完整的 options 对象。这个 options 对象会被传递给后续的插件或加载器,以便它们可以根据这些配置来执行相应的任务。
- 如果想要在Webpack中获取Shell脚本参数,可以使用Node.js的命令行参数解析库,例如yargs或commander
yargs
是一个非常有用的 Node.js 库,用于解析命令行参数,可以轻松地定义和解析命令行选项、参数和子命令,并将它们转换为 JavaScript 对象,这个对象可以作为参数传递给其他函数或流程,使得命令行参数的处理更加灵活和方便。
二、开始编译
-
webpack编译会创建两个核心对象
-
compiler:包含了webpack环境的所有的配置信息,包括options,loader和plugin,和webpack整个生命周期相关的钩子
-
在 lib/webpack.js 文件中,Compiler 对象的初始化逻辑主要包括以下几个步骤
① 创建 Compiler 实例:通过调用 new Compiler() 创建一个新的 Compiler 实例。
② 配置 Compiler:根据传入的配置对象(如 webpack.config.js 中定义的配置),对 Compiler 进行相应的配置,例如设置输出目录、加载器等。
③ 注册插件: 通过调用compiler.hooks上的方法,注册各种插件。这些插件可以在构建过程中执行各种自定义操作,如资源优化、代码分割等。
④ 初始化其他组件:根据配置 初始化其他与构建相关的组件,如Compilation、ResolverFactory等。
⑤ 返回 Compiler 实例:将初始化完成的 Compiler 实例返回,以便在构建过程中使用。
-
-
compilation:作为plugin内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的Compilation将被创建
-
-
当 Webpack 初始化完成后,Compiler对象会存储所有的配置和组件,但实际的构建过程还没有开始,要开始构建过程,需要调用Compiler对象的run方法。调用Compiler对象的run方法真正启动webpack构建流程,它是启动构建过程的最后一步,也是实际生成打包文件的开始
阶段
得到compiler对象
//Compiler类继承了Tapable。Compiler对象定义在./node_modules/webpack/lib/Compiler.js
//Tapable 是一个提供钩子(hooks)功能的基类,
//这些钩子允许我们在特定的事件点上执行自定义逻辑。
class Compiler extends Tapable {
constructor(context) {
//初始化时定义了很多钩子函数
super();
this.hooks = {
//生命周期钩子
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
make: new AsyncParallelHook(["compilation"]),
entryOption: new SyncBailHook(["context", "entry"])
// 定义了很多不同类型的钩子
};
// ...
}
}
//这是一个Webpack的外部API函数,它接受一个配置对象options作为参数。
//在函数内部创建了一个新的Compiler实例。这意味着使用Webpack时,实际上是在使用这个Compiler类来处理和打包资源。
function webpack(options) {
var compiler = new Compiler();
...// 检查options,若watch字段为true,则开启watch线程
return compiler;
}
...
-
compile阶段
-
整个编译过程的开始,意味着开始将源代码转换为可执行代码或目标代码,进行初步的解析和处理
-
make阶段
- 确定项目的依赖关系,并创建模块对象以供后续处理。会分析源代码中的import或require语句,分析项目的依赖关系,并确定哪些模块需要被编译
-
buildModule阶段
- 对每个模块的源代码进行处理、转换、优化、合并等操作
-
afterCompile
- 有模块都构建完成后执行一些全局的操作,可能会进行全局的代码优化、清理临时文件等
-
seal封装构建结果
- 将构建的结果进行封装,以便于部署和分发,工具会根据其内部的依赖关系,将每个chunk(例如,由多个模块组成的代码块)输出到指定的结果文件
-
emit发射文件
- 将各个模块或chunk输出到结果文件
-
after-emit
- 在所有chunks都输出到结果文件后执行一些后续操作,可能会复制生成的资源到输出目录、执行一些清理任务等
加载插件,执行run开始编译
- 执行了run()方法后首先会触发compile ,这一步会构建出Compilation 对象。该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象
确定入口
- 在创建module之前,Compiler会触发make,并调用Compilation.addEntry方法,通过options对象的entry字段找到我们的入口js文件
三、编译模块
-
从entry入口文件开始读取,主要执行_addModuleChain()函数。_addModuleChain 是 Webpack 的内部方法,用于处理模块链,能根据给定的依赖项创建新的模块实例。这个方法主要的工作是根据给定的 module 和 context 对象,以及一些其他参数,来创建一个新的模块。具体来说,这个方法会根据模块的类型(例如,JavaScript、CSS、图片等)来获取对应的模块工厂并创建模块。
-
_addModuleChain(context, dependency, onModule, callback) { ... // 根据依赖查找对应的工厂函数 const Dep = /** @type {DepConstructor} */ (dependency.constructor); const moduleFactory = this.dependencyFactories.get(Dep); // 调用工厂函数moduleFactory的create来生成一个空的NormalModule对象,NormalModul是Webpack中表示一个模块的类。 moduleFactory.create({ dependencies: [dependency] ... }, (err, module) => { ... //定义一个afterBuild函数,这个函数在模块编译完成后会被调用。 const afterBuild = () => { this.processModuleDependencies(module, err => { if (err) return callback(err);//处理过程中发生错误,则通过回调函数返回错误信息 callback(null, module); //处理成功,则通过回调函数返回新创建的模块 }); }; //开始模块编译。这里会调用buildModule方法来进行实际的编译操作。 this.buildModule(module, false, null, null, err => { ... afterBuild();//当模块编译完成后,调用之前定义的 afterBuild函数。 }) }) }// _addModuleChain 方法结束
-
_addModuleChain 是 Webpack 的内部方法,用于处理模块链。以下是该方法的主要过程:
- 接收参数:该方法接收四个参数,包括上下文对象 context、依赖项 dependency、回调函数 onModule 和回调函数 callback。
- 获取模块工厂:根据依赖项的构造类型,从 dependencyFactories 集合中获取对应的模块工厂。
- 创建新模块:使用获取到的模块工厂,通过 create方法创建一个新的模块实例。
- 添加模块到编译队列:将新创建的模块加入到编译对象的 modules 数组中,以便在后续的打包过程中使用该模块。
- 处理模块依赖:开始处理新创建模块的依赖项,递归地将它们也加入到编译队列中。
- 编译模块:当所有依赖项都处理完毕后,开始编译当前模块。Webpack 会调用 buildModule 方法来执行实际的编译操作,包括解析模块代码、生成依赖图等。
- 处理编译结果:当模块编译完成后,Webpack 会进行一些后续处理,例如将模块添加到编译对象的 modules 数组中,以便在后续的打包过程中使用该模块。
- 回调函数:当整个过程完成后,通过回调函数返回编译结果或错误信息。
随后执行buildModule进入真正的构建模块module内容的过程
调用loader,找到依赖
-
webpack提供的一个很大的便利就是能将所有资源都整合成模块,不仅仅是 js文件。因此,需要一些loader ,比如url-loader、jsx-loader、css-loader等,来让我们可以直接在源文件中引用各类资源。
-
对每一个require()用对应的loader进行加工
-
webpack调用doBuild(),对每一个require()用对应的loader进行加工,最后生成一个js module。当webpack遇到require()或import语句时,它会使用对应的加载器(loader)处理这些依赖项。加载器可以将源文件转换成Webpack能够理解和处理的模块。然后,Webpack会调用acorn来解析这些经过加载器处理的源文件,生成抽象语法树(AST)
-
// 定义在Compilation对象上的_addModuleChain方法,该方法用于处理模块链。 // 接收四个参数:context(上下文)、dependency(依赖项)、onModule(处理模块的回调函数)和callback(回调函数)。 Compilation.prototype._addModuleChain = function process(context, dependency, onModule, callback) { // 如果Compilation对象启用了性能分析,则记录当前时间,用于后续计算该方法的执行时间。 var start = this.profile && +new Date(); ... // 根据模块的类型获取对应的模块工厂并创建模块 // 根据传入的依赖项的构造函数,从dependencyFactories集合中获取对应的模块工厂。 // 这个集合中存储了各种类型的模块工厂,用于创建不同类型的模块。 var moduleFactory = this.dependencyFactories.get(dependency.constructor); ... moduleFactory.create(context, dependency, function(err, module) { var result = this.addModule(module); // 将新创建的模块添加到Compilation对象的modules数组中。 ... this.buildModule(module, function(err) { // 调用buildModule方法来构建模块。 ... // 构建模块,添加依赖模块 }.bind(this)); // 由于使用了bind(this)来绑定回调函数的执行上下文为Compilation对象,所以后续操作可以在Compilation对象的上下文中进行。 }.bind(this)); };
-
-
调用 acorn 解析经 loader 处理后的源文件,生成抽象语法树 AST
-
Acorn是一个轻量级、流式的JavaScript解析器,可以生成抽象语法树(AST)。通过Acorn解析源文件后,可以获得源文件的语法结构,以便进行进一步的分析、转换或打包。虽然加载器在处理源文件时起到重要作用,但AST的生成是由Acorn解析器完成的。
-
// 定义Parser对象的parse方法,该方法用于解析源代码并返回抽象语法树(AST)。 // 它接收两个参数:source(待解析的源代码)和initialState(初始状态)。 Parser.prototype.parse = function parse(source, initialState) { var ast; if (!ast) { // acorn以es6的语法进行解析 ast = acorn.parse(source, { ranges: true, // 解析时保留每个语法元素的原始位置范围信息。 locations: true, // 解析时保留每个语法元素的原始位置信息。 ecmaVersion: 6, // 使用ECMAScript 6(即ES6)的语法版本进行解析。 sourceType: "module" // 将源代码视为模块,解析为模块的抽象语法树(AST)。 }); } ... };
-
-
遍历AST,构建该模块所依赖的模块
-
对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历AST时,将require()中的模块通过addDependency()添加到数组中。当前模块构建完成后,webpack调用processModuleDependencies开始递归处理依赖的module,接着就会重复之前的构建步骤。
-
从配置的入口模块开始,分析其 AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系
-
// 定义Compilation对象的addModuleDependencies方法,用于向给定的模块添加依赖。 // 它接收五个参数:module(要添加依赖的模块)、dependencies(依赖数组)、bail(是否立即停止编译)、cacheGroup(缓存组)和recursive(是否递归添加依赖)。 // 最后一个参数callback是回调函数,用于在添加依赖完成时进行回调。 Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) { // 根据依赖数组(dependencies)创建依赖模块对象 var factories = []; // 用于存储依赖模块的工厂对象 for (var i = 0; i < dependencies.length; i++) { // 为每个依赖创建一个工厂对象并存储到factories数组中。 var factory = _this.dependencyFactories.get(dependencies[i][0].constructor); // 获取对应的模块工厂 factories[i] = [factory, dependencies[i]]; // 将工厂对象和依赖项一起存储到factories数组中。 } ... // 与当前模块构建步骤相同 }
-
四、完成编译
- module是webpack构建的核心实体,也是所有module的父类,它有几种不同子类:NormalModule,MultiModule,ContextModule,DelegatedModule 等。但这些核心实体都是在构建中都会去调用对应方法,也就是模块构建函数build()。
- 对于每一个module,它都会有这样一个构建方法build()。当然,它还包括了从构建到输出的一系列的有关module生命周期的函数。
五、输出资源
seal 输出资源
- 在所有模块及其依赖模块编译(build)完成后,webpack会触发一个名为seal的生命周期事件,这个事件标志着构建过程的结束,并且webpack将开始对构建后的结果进行封装。在seal事件触发后,webpack会逐个对每个module和chunk进行整理,这个阶段的目标是生成编译后的源码,进行合并、拆分以及生成hash
- seal方法主要是生成chunks,对chunks进行一系列的优化操作,并生成要输出的代码。webpack中的chunk,可以理解为配置在entry中的模块,或者是动态引入的模块,根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表
// 定义Compilation对象的seal方法,该方法接受一个回调函数作为参数
Compilation.prototype.seal = function seal(callback) {
this.applyPlugins("seal");//调用并触发所有已注册的插件的seal事件。这允许插件执行一些自定义逻辑或操作
this.preparedChunks.sort(function(a, b) {
//对已准备的chunks进行排序。排序的依据是chunk的名称。
if (a.name < b.name) {
return -1;//如果名称a小于名称b,则返回-1;
}
if (a.name > b.name) {
return 1;//如果名称a大于名称b,则返回1;
}
return 0;//如果两者相等,则返回0
});
// 遍历已排序的chunks,并对每个chunk执行以下操作:
this.preparedChunks.forEach(function(preparedChunk) {
var module = preparedChunk.module;//获取与当前chunk关联的模块
var chunk = this.addChunk(preparedChunk.name, module);//向当前的chunks集合中添加一个新的chunk,chunk的名称为preparedChunk.name,并且与module关联
chunk.initial = chunk.entry = true;//设置新添加的chunk的一些属性:它是一个初始chunk,也是一个入口chunk
// 整理每个Module和chunk,每个chunk对应一个输出文件。将module添加到chunk中,并把chunk添加到module中。这表示module和chunk之间存在依赖关系
chunk.addModule(module);
module.addChunk(chunk);
}, this);//使用bind确保回调函数中的this指向Compilation对象实例
//异步调用并触发所有已注册的插件的"optimize-tree"事件。这个事件允许插件对chunks和modules进行优化。优化完成后,会执行回调函数。
this.applyPluginsAsync("optimize-tree", this.chunks, this.modules, function(err) {
if (err) {
return callback(err);//如果回调函数中发生错误,则将错误传递给外部的callback函数
}
... // 触发插件的事件
this.createChunkAssets(); //生成最终assets,可能包括JavaScript、CSS、图片等。生成的assets会被存储在compiler的assets属性中。
... // 触发插件的事件
}.bind(this));// 使用bind确保回调函数中的this指向Compilation对象实例
};// seal方法结束
六、写入文件
存储到compiler.assets对象
- 在封装过程中,webpack会调用Compilation中的createChunkAssets方法进行打包后代码的生成。createChunkAssets函数通过收集和封装代码片段,将它们转化为可执行的模块,并将这些资源文件存储在compiler.assets对象中,以便后续的输出和引用。createChunkAssets流程如下
-
createChunkAssets的流程主要包括以下步骤:
① 判断是否是入口chunk:createChunkAssets函数首先判断当前处理的chunk是否是入口chunk。如果是入口chunk,则使用MainTemplate进行封装,否则使用ChunkTemplate进行封装。
② 处理依赖关系:在语法树解析阶段,createChunkAssets函数会收集文件的依赖关系,包括需要插入或替换的部分。
③ 生成代码片段:在generate阶段,createChunkAssets函数会生成各种代码片段,包括需要打包输出的资源。
④ 封装代码:createChunkAssets函数会对生成的代码片段进行收集和封装,将它们封装成可执行的模块。一旦代码片段被封装完毕,它们就会被存储在compiler.assets对象中。
emit输出
-
在Webpack的打包过程中,createChunkAssets方法执行完毕后,会调用Compiler中的emitAssets()方法。所以最后一步是,webpack调用Compiler中的emitAssets(),将生成的代码输入到output的指定位置,完成最终文件的输出,从而webpack整个打包过程结束。
-
output对象在Webpack配置中定义了输出目录、文件名等选项,emitAssets()方法会将生成的代码按照output配置的规则输出到相应的目录中。输出的文件可以是JavaScript、CSS、图片等资源文件,以及其他依赖图和其他元数据。( 在确定好输出内容后,根据配置确定输出的路径和文件名)
-
若想对结果进行处理,则需要在emitAssets()触发后对自定义插件进行扩展,可以通过编写自定义插件来实现。例如,可以编写一个自定义插件,并在emitAssets()触发后对生成的代码进行进一步处理或修改。在插件的apply方法中,可以访问compiler对象并监听emit事件。当emit事件触发时,可以执行自己的逻辑来处理生成的代码或资源
-
具体来说,可以在插件中实现以下步骤