likes
comments
collection
share

webpack工作流程webpack工作流程 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置

作者站长头像
站长
· 阅读数 89

webpack工作流程

  1. 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
  2. 用上一步得到的参数初始化 Compiler 对象
  3. 加载所有配置的插件
  4. 执行对象的 run 方法开始执行编译
  5. 根据配置中的entry找出入口文件
  6. 从入口文件出发,调用所有配置的Loader对模块进行编译
  7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  8. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
  9. 再把每个 Chunk 转换成一个单独的文件加入到输出列表
  10. 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

webpack工作流程webpack工作流程 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置

一、初始化参数

  • 从配置文件和 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.jsonscript中配置对应的命令

参数解析

  • 为了整合 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开始编译

webpack工作流程webpack工作流程 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置

  • 执行了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 的内部方法,用于处理模块链。以下是该方法的主要过程:

    1. 接收参数:该方法接收四个参数,包括上下文对象 context、依赖项 dependency、回调函数 onModule 和回调函数 callback。
    2. 获取模块工厂:根据依赖项的构造类型,从 dependencyFactories 集合中获取对应的模块工厂。
    3. 创建新模块:使用获取到的模块工厂,通过 create方法创建一个新的模块实例。
    4. 添加模块到编译队列:将新创建的模块加入到编译对象的 modules 数组中,以便在后续的打包过程中使用该模块。
    5. 处理模块依赖:开始处理新创建模块的依赖项,递归地将它们也加入到编译队列中。
    6. 编译模块:当所有依赖项都处理完毕后,开始编译当前模块。Webpack 会调用 buildModule 方法来执行实际的编译操作,包括解析模块代码、生成依赖图等。
    7. 处理编译结果:当模块编译完成后,Webpack 会进行一些后续处理,例如将模块添加到编译对象的 modules 数组中,以便在后续的打包过程中使用该模块。
    8. 回调函数:当整个过程完成后,通过回调函数返回编译结果或错误信息。 随后执行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流程如下

webpack工作流程webpack工作流程 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置

  • 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(),返回的是主要含有moduleschunksassets三个属性值的对象。
  • 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": []
}
转载自:https://juejin.cn/post/7381412483040313363
评论
请登录