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事件触发时,可以执行自己的逻辑来处理生成的代码或资源
-
具体来说,可以在插件中实现以下步骤:
①访问compiler对象并监听emit事件。
②在emit事件触发时,获取生成的代码或资源。
③对获取的代码或资源进行进一步处理或修改。
④如果你想将修改后的结果输出到文件系统,可以使用compiler.outputFileSystem来写入文件。
⑤完成处理后,你可以选择将修改后的结果重新赋值给compiler.assets对象,以便Webpack将其输出到指定的目录中。
调试 webpack
通过 chrome 调试
node --inspect-brk ./node_modules/webpack-cli/bin/cli.js
然后打开 Chrome 浏览器控制台就可以调试了
通过执行命令调试
- 打开工程目录,点击调试按钮,再点击小齿轮的配置按钮系统就会生成
launch.json
配置文件 - 修改好了以后直接点击 F5 就可以启动调试
.vscode\launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "debug webpack",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/node_modules/webpack-cli/bin/cli.js"
}
]
}
debugger.js
const webpack = require("webpack");
const webpackOptions = require("./webpack.config");
const compiler = webpack(webpackOptions);
//4.执行对象的run方法开始执行编译
compiler.run((err, stats) => {
console.log(err);
console.log(
stats.toJson({
assets: true,
chunks: true,
modules: true,
})
);
});
tapable.js
- tapable 是一个类似于 Node.js 中的 EventEmitter 的库,但更专注于自定义事件的触发和处理
- webpack 通过 tapable 将实现与流程解耦,所有具体实现通过插件的形式存在
class SyncHook {
constructor() {
this.taps = [];
}
tap(name, fn) {
this.taps.push(fn);
}
call() {
this.taps.forEach((tap) => tap());
}
}
let hook = new SyncHook();
hook.tap("some name", () => {
console.log("some name");
});
class Plugin {
apply() {
hook.tap("Plugin", () => {
console.log("Plugin ");
});
}
}
new Plugin().apply();
hook.call();
webpack 编译流程
4.1 debugger.js
debugger.js
const webpack = require("./webpack");
const options = require("./webpack.config");
const compiler = webpack(options);
compiler.run((err, stats) => {
console.log(err);
console.log(
JSON.stringify(
stats.toJson({
assets: true, //资源
chunks: true, //代码块
modules: true, //模块
}),
null,
2
)
);
});
4.2 webpack.config.js
webpack.config.js
const path = require("path");
const RunPlugin = require("./plugins/run-plugin");
const DonePlugin = require("./plugins/done-plugin");
module.exports = {
mode: "development",
devtool: false,
entry: {
entry1: "./src/entry1.js",
entry2: "./src/entry2.js",
},
output: {
path: path.resolve("dist"),
filename: "[name].js",
},
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx", ".json"],
},
module: {
rules: [
{
test: /\.js$/,
use: [
path.resolve(__dirname, "loaders/logger1-loader.js"),
path.resolve(__dirname, "loaders/logger2-loader.js"),
],
},
],
},
plugins: [
new RunPlugin(), //开始编译的时候触发run事件,RunPlugin会监听这个事件执行回调
new DonePlugin(), //编译完成的时候会触发done事件,DonePlugin会监听这个done事件的回调
],
};
4.3 webpack.js
webpack.js
let Compiler = require("./Compiler");
function webpack(options) {
//1.初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象
console.log(process.argv); //['node.exe','debugger.js']
let argv = process.argv.slice(2);
let shellOptions = argv.reduce((shellOptions, option) => {
let [key, value] = option.split("=");
shellOptions[key.slice(2)] = value;
return shellOptions;
}, {});
let finalOptions = { ...options, ...shellOptions };
console.log("finalOptions", finalOptions);
//2.用上一步得到的参数初始化Compiler对象
let compiler = new Compiler(finalOptions);
//3.加载所有配置的插件
let { plugins } = finalOptions;
for (let plugin of plugins) {
plugin.apply(compiler);
}
return compiler;
}
module.exports = webpack;
4.4 Compiler.js
Compiler.js
let { SyncHook } = require("tapable");
let fs = require("fs");
let path = require("path");
let Complication = require("./Complication");
/**
* Compiler就是编译大管家
* 负责整个编译过程,里面保存整个编译所有的信息
*/
class Compiler {
constructor(options) {
this.options = options;
this.hooks = {
run: new SyncHook(), //会在开始编译的时候触发
done: new SyncHook(), //会在结束编译的时候触发
};
}
//4.执行Compiler对象的run方法开始执行编译
run(callback) {
this.hooks.run.call();
//5.根据配置中的entry找出入口文件
const onCompiled = (err, stats, fileDependencies) => {
//10在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
for (let filename in stats.assets) {
let filePath = path.join(this.options.output.path, filename);
fs.writeFileSync(filePath, stats.assets[filename], "utf8");
}
callback(err, {
toJson: () => stats,
});
fileDependencies.forEach((fileDependency) =>
fs.watch(fileDependency, () => this.compile(onCompiled))
);
};
this.compile(onCompiled);
this.hooks.done.call();
}
compile(callback) {
//每次编译都会创建一个新的Compilcation
let complication = new Complication(this.options);
complication.build(callback);
}
}
module.exports = Compiler;
4.5 Complication.js
Complication.js
let fs = require("fs");
let types = require("babel-types");
let parser = require("@babel/parser");
let traverse = require("@babel/traverse").default;
let generator = require("@babel/generator").default;
const path = require("path");
//根目录就是当前的工作目录
let baseDir = toUnixPath(process.cwd()); // \ => /
function toUnixPath(filePath) {
return filePath.replace(/\\/g, "/");
}
class Complication {
constructor(options) {
this.options = options;
this.modules = []; //存放着本次编译生产所有的模块 所有的入口产出的模块
this.chunks = []; //代码块的数组
this.assets = {}; //产出的资源
this.fileDependencies = [];
}
//这个才是编译最核心的方法
build(callback) {
//5.根据配置中的entry找出入口文件
let entry = {};
if (typeof this.options.entry === "string") {
entry.main = this.options.entry;
} else {
entry = this.options.entry;
}
for (let entryName in entry) {
//找到入口文件的绝对路径
let entryFilePath = path.posix.join(baseDir, entry[entryName]);
this.fileDependencies.push(entryFilePath);
//6.从入口文件出发,调用所有配置的Loader对模块进行编译
let entryModule = this.buildModule(entryName, entryFilePath);
//8.根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
let chunk = {
name: entryName, //代码块的名字就是入口的名字
entryModule, //入口模块 entry1.js
modules: this.modules.filter((item) => item.name.includes(entryName)),
};
this.chunks.push(chunk);
}
//9.再把每个Chunk转换成一个单独的文件加入到输出列表
this.chunks.forEach((chunk) => {
let filename = this.options.output.filename.replace("[name]", chunk.name);
this.assets[filename] = getSource(chunk);
});
callback(
null,
{
chunks: this.chunks,
modules: this.modules,
assets: this.assets,
},
this.fileDependencies
);
}
//name此模块是属于哪个入口的 modulePath 模块的绝对路径
buildModule(name, modulePath) {
//6.从入口文件出发,调用所有配置的Loader对模块进行编译
//1.读取模块的内容
let sourceCode = fs.readFileSync(modulePath, "utf8");
let { rules } = this.options.module;
let loaders = []; //
rules.forEach((rule) => {
let { test } = rule;
if (modulePath.match(test)) {
loaders.push(...rule.use);
}
}); //loaders=[logger1,logger2]
sourceCode = loaders.reduceRight((sourceCode, loader) => {
return require(loader)(sourceCode);
}, sourceCode);
//当前模块的模块ID
let moduleId = "./" + path.posix.relative(baseDir, modulePath);
let module = { id: moduleId, dependencies: [], name: [name] };
//7.再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
let ast = parser.parse(sourceCode, { sourceType: "module" });
traverse(ast, {
CallExpression: ({ node }) => {
if (node.callee.name === "require") {
//获取依赖模块的相对路径 wepback打包后不管什么模块,模块ID都是相对于根目录的相对路径 ./src ./node_modules
let depModuleName = node.arguments[0].value; // ./title
//获取当前模块的所在的目录
let dirname = path.posix.dirname(modulePath); //src
//C:\aproject\hswebpack202308\4.flow\src\title.js
let depModulePath = path.posix.join(dirname, depModuleName);
let extensions = this.options.resolve.extensions;
depModulePath = tryExtensions(depModulePath, extensions);
this.fileDependencies.push(depModulePath);
//生成此模块的模块ID
let depModuleId = "./" + path.posix.relative(baseDir, depModulePath);
node.arguments = [types.stringLiteral(depModuleId)]; // ./title => ./src/title.js
//把此模块依赖的模块ID和模块路径放到此模块的依赖数组中
module.dependencies.push({ depModuleId, depModulePath });
}
},
});
let { code } = generator(ast); //根据改造后的语法树生成源代码
module._source = code; //module._source属必指向此模块的改造后的源码
//7.再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
module.dependencies.forEach(({ depModuleId, depModulePath }) => {
let existModule = this.modules.find((item) => item.id === depModuleId);
if (existModule) {
existModule.name.push(name);
} else {
let depModule = this.buildModule(name, depModulePath);
this.modules.push(depModule);
}
});
return module;
}
}
function tryExtensions(modulePath, extensions) {
if (fs.existsSync(modulePath)) {
return modulePath;
}
for (let i = 0; i < extensions.length; i++) {
let filePath = modulePath + extensions[i];
if (fs.existsSync(filePath)) {
return filePath;
}
}
throw new Error(`${modulePath}没找到`);
}
function getSource(chunk) {
return `
(() => {
var modules = {
${chunk.modules.map(
(module) => `
"${module.id}": (module) => {
${module._source}
},
`
)}
};
var cache = {};
function require(moduleId) {
var cachedModule = cache[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (cache[moduleId] = {
exports: {},
});
modules[moduleId](module, module.exports, require);
return module.exports;
}
var exports ={};
${chunk.entryModule._source}
})();
`;
}
module.exports = Complication;
4.6 run-plugin.js
plugins\run-plugin.js
class RunPlugin {
//每个插件都是一个类,而每个类都需要定义一个apply方法
apply(compiler) {
compiler.hooks.run.tap("RunPlugin", () => {
console.log("run:开始编译");
});
}
}
module.exports = RunPlugin;
4.7 done-plugin.js
plugins\done-plugin.js
class DonePlugin {
//每个插件都是一个类,而每个类都需要定义一个apply方法
apply(compiler) {
compiler.hooks.done.tap("DonePlugin", () => {
console.log("done:结束编译");
});
}
}
module.exports = DonePlugin;
4.9 logger1-loader.js
loaders\logger1-loader.js
function loader(source) {
return source + "//logger1"; //let name= 'entry1';//logger2//logger1
}
module.exports = loader;
4.10 logger2-loader.js
loaders\logger2-loader.js
function loader(source) {
//let name= 'entry1';
return source + "//logger2"; //let name= 'entry1';//logger2
}
module.exports = loader;
4.11 src\entry1.js
src\entry1.js
let title = require("./title");
console.log("entry12", title);
4.12 src\entry2.js
src\entry2.js
let title = require("./title.js");
console.log("entry2", title);
4.13 src\title.js
src\title.js
module.exports = "title";
Stats 对象
- 在 Webpack 的回调函数中会得到 stats 对象
- 这个对象实际来自于
Compilation.getStats()
,返回的是主要含有modules
、chunks
和assets
三个属性值的对象。 - Stats 对象本质上来自于lib/Stats.js的类实例
字段 | 含义 |
---|---|
modules | 记录了所有解析后的模块 |
chunks | 记录了所有 chunk |
assets | 记录了所有要生成的文件 |
npx webpack --profile --json > stats.json
{
"hash": "780231fa9b9ce4460c8a", //编译使用的 hash
"version": "5.8.0", // 用来编译的 webpack 的版本
"time": 83, // 编译耗时 (ms)
"builtAt": 1606538839612, //编译的时间
"publicPath": "auto", //资源访问路径
"outputPath": "C:\\webpack5\\dist", //输出目录
"assetsByChunkName": {
//代码块和文件名的映射
"main": ["main.js"]
},
"assets": [
//资源数组
{
"type": "asset", //资源类型
"name": "main.js", //文件名称
"size": 2418, //文件大小
"chunkNames": [
//对应的代码块名称
"main"
],
"chunkIdHints": [],
"auxiliaryChunkNames": [],
"auxiliaryChunkIdHints": [],
"emitted": false,
"comparedForEmit": true,
"cached": false,
"info": {
"javascriptModule": false,
"size": 2418
},
"related": {},
"chunks": ["main"],
"auxiliaryChunks": [],
"isOverSizeLimit": false
}
],
"chunks": [
//代码块数组
{
"rendered": true,
"initial": true,
"entry": true,
"recorded": false,
"size": 80,
"sizes": {
"javascript": 80
},
"names": ["main"],
"idHints": [],
"runtime": ["main"],
"files": ["main.js"],
"auxiliaryFiles": [],
"hash": "d25ad7a8144077f69783",
"childrenByOrder": {},
"id": "main",
"siblings": [],
"parents": [],
"children": [],
"modules": [
{
"type": "module",
"moduleType": "javascript/auto",
"identifier": "C:\\webpack5\\src\\index.js",
"name": "./src/index.js",
"nameForCondition": "C:\\webpack5\\src\\index.js",
"index": 0,
"preOrderIndex": 0,
"index2": 1,
"postOrderIndex": 1,
"size": 55,
"sizes": {
"javascript": 55
},
"cacheable": true,
"built": true,
"codeGenerated": true,
"cached": false,
"optional": false,
"orphan": false,
"dependent": false,
"issuer": null,
"issuerName": null,
"issuerPath": null,
"failed": false,
"errors": 0,
"warnings": 0,
"profile": {
"total": 38,
"resolving": 26,
"restoring": 0,
"building": 12,
"integration": 0,
"storing": 0,
"additionalResolving": 0,
"additionalIntegration": 0,
"factory": 26,
"dependencies": 0
},
"id": "./src/index.js",
"issuerId": null,
"chunks": ["main"],
"assets": [],
"reasons": [
{
"moduleIdentifier": null,
"module": null,
"moduleName": null,
"resolvedModuleIdentifier": null,
"resolvedModule": null,
"type": "entry",
"active": true,
"explanation": "",
"userRequest": "./src/index.js",
"loc": "main",
"moduleId": null,
"resolvedModuleId": null
}
],
"usedExports": null,
"providedExports": null,
"optimizationBailout": [],
"depth": 0
},
{
"type": "module",
"moduleType": "javascript/auto",
"identifier": "C:\\webpack5\\src\\title.js",
"name": "./src/title.js",
"nameForCondition": "C:\\webpack5\\src\\title.js",
"index": 1,
"preOrderIndex": 1,
"index2": 0,
"postOrderIndex": 0,
"size": 25,
"sizes": {
"javascript": 25
},
"cacheable": true,
"built": true,
"codeGenerated": true,
"cached": false,
"optional": false,
"orphan": false,
"dependent": true,
"issuer": "C:\\webpack5\\src\\index.js",
"issuerName": "./src/index.js",
"issuerPath": [
{
"identifier": "C:\\webpack5\\src\\index.js",
"name": "./src/index.js",
"profile": {
"total": 38,
"resolving": 26,
"restoring": 0,
"building": 12,
"integration": 0,
"storing": 0,
"additionalResolving": 0,
"additionalIntegration": 0,
"factory": 26,
"dependencies": 0
},
"id": "./src/index.js"
}
],
"failed": false,
"errors": 0,
"warnings": 0,
"profile": {
"total": 0,
"resolving": 0,
"restoring": 0,
"building": 0,
"integration": 0,
"storing": 0,
"additionalResolving": 0,
"additionalIntegration": 0,
"factory": 0,
"dependencies": 0
},
"id": "./src/title.js",
"issuerId": "./src/index.js",
"chunks": ["main"],
"assets": [],
"reasons": [
{
"moduleIdentifier": "C:\\webpack5\\src\\index.js",
"module": "./src/index.js",
"moduleName": "./src/index.js",
"resolvedModuleIdentifier": "C:\\webpack5\\src\\index.js",
"resolvedModule": "./src/index.js",
"type": "cjs require",
"active": true,
"explanation": "",
"userRequest": "./title.js",
"loc": "1:12-33",
"moduleId": "./src/index.js",
"resolvedModuleId": "./src/index.js"
},
{
"moduleIdentifier": "C:\\webpack5\\src\\title.js",
"module": "./src/title.js",
"moduleName": "./src/title.js",
"resolvedModuleIdentifier": "C:\\webpack5\\src\\title.js",
"resolvedModule": "./src/title.js",
"type": "cjs self exports reference",
"active": true,
"explanation": "",
"userRequest": null,
"loc": "1:0-14",
"moduleId": "./src/title.js",
"resolvedModuleId": "./src/title.js"
}
],
"usedExports": null,
"providedExports": null,
"optimizationBailout": [
"CommonJS bailout: module.exports is used directly at 1:0-14"
],
"depth": 1
}
],
"origins": [
{
"module": "",
"moduleIdentifier": "",
"moduleName": "",
"loc": "main",
"request": "./src/index.js"
}
]
}
],
"modules": [
//模块数组
{
"type": "module",
"moduleType": "javascript/auto",
"identifier": "C:\\webpack5\\src\\index.js",
"name": "./src/index.js",
"nameForCondition": "C:\\webpack5\\src\\index.js",
"index": 0,
"preOrderIndex": 0,
"index2": 1,
"postOrderIndex": 1,
"size": 55,
"sizes": {
"javascript": 55
},
"cacheable": true,
"built": true,
"codeGenerated": true,
"cached": false,
"optional": false,
"orphan": false,
"issuer": null,
"issuerName": null,
"issuerPath": null,
"failed": false,
"errors": 0,
"warnings": 0,
"profile": {
"total": 38,
"resolving": 26,
"restoring": 0,
"building": 12,
"integration": 0,
"storing": 0,
"additionalResolving": 0,
"additionalIntegration": 0,
"factory": 26,
"dependencies": 0
},
"id": "./src/index.js",
"issuerId": null,
"chunks": ["main"],
"assets": [],
"reasons": [
{
"moduleIdentifier": null,
"module": null,
"moduleName": null,
"resolvedModuleIdentifier": null,
"resolvedModule": null,
"type": "entry",
"active": true,
"explanation": "",
"userRequest": "./src/index.js",
"loc": "main",
"moduleId": null,
"resolvedModuleId": null
}
],
"usedExports": null,
"providedExports": null,
"optimizationBailout": [],
"depth": 0
},
{
"type": "module",
"moduleType": "javascript/auto",
"identifier": "C:\\webpack5\\src\\title.js",
"name": "./src/title.js",
"nameForCondition": "C:\\webpack5\\src\\title.js",
"index": 1,
"preOrderIndex": 1,
"index2": 0,
"postOrderIndex": 0,
"size": 25,
"sizes": {
"javascript": 25
},
"cacheable": true,
"built": true,
"codeGenerated": true,
"cached": false,
"optional": false,
"orphan": false,
"issuer": "C:\\webpack5\\src\\index.js",
"issuerName": "./src/index.js",
"issuerPath": [
{
"identifier": "C:\\webpack5\\src\\index.js",
"name": "./src/index.js",
"profile": {
"total": 38,
"resolving": 26,
"restoring": 0,
"building": 12,
"integration": 0,
"storing": 0,
"additionalResolving": 0,
"additionalIntegration": 0,
"factory": 26,
"dependencies": 0
},
"id": "./src/index.js"
}
],
"failed": false,
"errors": 0,
"warnings": 0,
"profile": {
"total": 0,
"resolving": 0,
"restoring": 0,
"building": 0,
"integration": 0,
"storing": 0,
"additionalResolving": 0,
"additionalIntegration": 0,
"factory": 0,
"dependencies": 0
},
"id": "./src/title.js",
"issuerId": "./src/index.js",
"chunks": ["main"],
"assets": [],
"reasons": [
{
"moduleIdentifier": "C:\\webpack5\\src\\index.js",
"module": "./src/index.js",
"moduleName": "./src/index.js",
"resolvedModuleIdentifier": "C:\\webpack5\\src\\index.js",
"resolvedModule": "./src/index.js",
"type": "cjs require",
"active": true,
"explanation": "",
"userRequest": "./title.js",
"loc": "1:12-33",
"moduleId": "./src/index.js",
"resolvedModuleId": "./src/index.js"
},
{
"moduleIdentifier": "C:\\webpack5\\src\\title.js",
"module": "./src/title.js",
"moduleName": "./src/title.js",
"resolvedModuleIdentifier": "C:\\webpack5\\src\\title.js",
"resolvedModule": "./src/title.js",
"type": "cjs self exports reference",
"active": true,
"explanation": "",
"userRequest": null,
"loc": "1:0-14",
"moduleId": "./src/title.js",
"resolvedModuleId": "./src/title.js"
}
],
"usedExports": null,
"providedExports": null,
"optimizationBailout": [
"CommonJS bailout: module.exports is used directly at 1:0-14"
],
"depth": 1
}
],
"entrypoints": {
//入口点
"main": {
"name": "main",
"chunks": ["main"],
"assets": [
{
"name": "main.js",
"size": 2418
}
],
"filteredAssets": 0,
"assetsSize": 2418,
"auxiliaryAssets": [],
"filteredAuxiliaryAssets": 0,
"auxiliaryAssetsSize": 0,
"children": {},
"childAssets": {},
"isOverSizeLimit": false
}
},
"namedChunkGroups": {
//命名代码块组
"main": {
"name": "main",
"chunks": ["main"],
"assets": [
{
"name": "main.js",
"size": 2418
}
],
"filteredAssets": 0,
"assetsSize": 2418,
"auxiliaryAssets": [],
"filteredAuxiliaryAssets": 0,
"auxiliaryAssetsSize": 0,
"children": {},
"childAssets": {},
"isOverSizeLimit": false
}
},
"errors": [],
"errorsCount": 0,
"warnings": [],
"warningsCount": 0,
"children": []
}