webpack工作原理

news2025/1/11 7:50:57

目录

    • 合并代码
    • 模块化
    • webpack 的打包
    • webpack 的结构
    • webpack 的源码
      • addEntry 和 _addModuleChain
      • buildModule
      • Compilation 的钩子
      • 产出构建结果

了解 webpack 实现原理,掌握 webpack 基础的工作流程,在平时使用 webpack
遇见问题时,能够帮助我们洞察问题的根本所在,能够帮助我们理清解决问题的基本思路,同时也有助于我们更好地理解 loader 和 plugin
的使用和意义,在处理一些定制化的构建需求时更加得心应手。

抛开 webpack 复杂的 loader 和 plugin 机制,webpack 本质上就是一个 JS 模块 Bundler,用于将多个代码模块进行打包,所以我们先撇开 webpack 错综复杂的整体实现,来看一下一个相对简单的 JS 模块 Bunlder 的基础工作流程是怎么样的,在了解了 bundler 如何工作的基础上,再进一步去整理 webpack 整个流程,将 loader 和 plugin 的机制弄明白。
在这里插入图片描述

合并代码

在 js 的模块解决方案出来之前,一些前端类库为了拆分代码文件,会编写简单的合并代码文件的工具,有序得将多个代码文件合并到一起成为最终的 js 文件。举个例子:

// A.js
function A() {}

// B.js
function B() {}

// 结果
function A() {}
function B() {
  // 这里可以用 A 方法
}

这样的合并脚本很简单,读者们可以尝试用 Node 写一个。这样简单合并的方法有很明显的缺点,在代码库越来越大的时候会变得难以维护:

  • 文件合并时的顺序很难确定
  • 代码文件内变量和方法命名容易冲突

为了解决上述的问题,js 的模块解决方案就出来了,发展到今日,webpack 就是前端社区里最受欢迎的的 JS 模块 bundler。下边我们看下 webpack 是如何解决上述这两个问题的。

模块化

第一个问题,模块文件合并到一起时位置的顺序,简单理解也就是模块代码的执行顺序。CommonJS 规范和 ES Module 规范定义的就是在模块中声明依赖的方式,我们在模块中写下这样的代码:

// entry.js
import { bar } from './bar.js'; // 依赖 ./bar.js 模块

// bar.js
const foo = require('./foo.js'); // 依赖 ./foo.js 模块

便是在声明当前模块代码(即是入口的 entry.js)要执行时,需要依赖于「bar.js」模块的执行,而「bar.js」这个模块则依赖于「foo.js」。bundler 需要从这个入口代码(第一段)中解析出依赖 bar.js,然后再读取 bar.js 这个代码文件,解析出依赖 foo.js 代码文件,继续解析其依赖,递归下去,直至没有更多的依赖模块,最终形成一颗模块依赖树。

依赖解析和管理便是 webpack 这个 bundler 很重要的一个工作。如果 foo.js 文件没有依赖其他的模块的话,那么这个简单例子的依赖树也就相对简单:entry.js -> bar.js -> foo.js,当然,日常开发中遇见的一般都是相当复杂的代码模块依赖关系。

如果放到合并文件的处理上,上述的「foo.js」和「bar.js」模块的代码需要放到我们入口代码的前边。但是 webpack 不是简单地按照依赖的顺序合并,而是采取了一种更加巧妙的方式,顺带解决了前边提到的文件合并的第二个问题。

webpack 的打包

在已经解析出依赖关系的前提下,webpack 会利用 JavaScript Function 的特性提供一些代码来将各个模块整合到一起,即是将每一个模块包装成一个 JS Function,提供一个引用依赖模块的方法,如下面例子中的 __webpack__require__,这样做,既可以避免变量相互干扰,又能够有效控制执行顺序,简单的代码例子如下:

// 分别将各个依赖模块的代码用 modules 的方式组织起来打包成一个文件
// entry.js
modules['./entry.js'] = function() {
  const { bar } = __webpack__require__('./bar.js')
}

// bar.js
modules['./bar.js'] = function() {
  const foo = __webpack__require__('./foo.js')
};

// foo.js
modules['./foo.js'] = function() {
  // ...
}

// 已经执行的代码模块结果会保存在这里
const installedModules = {}

function __webpack__require__(id) {
  // ... 
  // 如果 installedModules 中有就直接获取
  // 没有的话从 modules 中获取 function 然后执行,将结果缓存在 installedModules 中然后返回结果
}

我们前边介绍 webpack 生成 js 代码体积大小优化时,介绍过有一个配置可以用于移除掉部分 function 代码,因为这种实现方式最大的缺点就是会增大生成的 js 代码体积,当 webpack 可以确定代码执行顺序,以及可以用唯一的模块 id 去调整模块内变量名防止冲突时,这些所谓的胶水代码也就没有必要存在了。

关于不使用 function 包裹模块代码的实现方式,有兴趣的同学可以看看这个文章:Webpack and Rollup: the same but different,也可以使用 rollup 来构建一个简单的例子,看看生成的代码结果。

webpack 的结构

webpack 需要强大的扩展性,尤其是插件实现这一块,webpack 利用了 tapable 这个库(其实也是 webpack 作者开发的库)来协助实现对于整个构建流程各个步骤的控制。

关于这个库更多的使用内容可以去查看官方的文档:tapable,使用上并不算十分复杂,最主要的功能就是用来添加各种各样的钩子方法(即 Hook)。

webpack 基于 tapable 定义了主要构建流程后,使用 tapable 这个库添加了各种各样的钩子方法来将 webpack 扩展至功能十分丰富,同时对外提供了相对强大的扩展性,即 plugin 的机制。

在这个基础上,我们来了解一下 webpack 工作的主要流程和其中几个重要的概念。

  • Compiler,webpack 的运行入口,实例化时定义 webpack 构建主要流程,同时创建构建时使用的核心对象 compilation
  • Compilation,由 Compiler 实例化,存储构建过程中各流程使用到的数据,用于控制这些数据的变化
  • Chunk,即用于表示 chunk 的类,即构建流程中的主干,一般情况下一个入口会对应一个 chunk,对于构建时需要的 chunk 对象由 Compilation 创建后保存管理
  • Module,用于表示代码模块的类,衍生出很多子类用于处理不同的情况,关于代码模块的所有信息都会存在 Module 实例中,例如 dependencies 记录代码模块的依赖等
  • Parser,其中相对复杂的一个部分,基于 acorn 来分析 AST 语法树,解析出代码模块的依赖
  • Dependency,解析时用于保存代码模块对应的依赖使用的对象
  • Template,生成最终代码要使用到的代码模板,像上述提到的 function 代码就是用对应的 Template 来生成

官方对于 Compiler 和 Compilation 的定义是:

compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。

compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键步骤的回调,以供插件做自定义处理时选择使用。

上述是 webpack 源码实现中比较重要的几个部分,webpack 运行的大概工作流程是这样的:

创建 Compiler -> 
调用 compiler.run 开始构建 ->
创建 Compilation -> 
基于配置开始创建 Chunk -> 
使用 Parser 从 Chunk 开始解析依赖 -> 
使用 Module 和 Dependency 管理代码模块相互关系 -> 
使用 Template 基于 Compilation 的数据生成结果代码 ->

上述只是博主理解中的大概流程,细节相对复杂,一方面是技术实现的细节有一定复杂度,另一方面是实现的功能逻辑上也有一定复杂度,深入介绍的话,篇幅会很长,并且可能效果不理想,当我们还没到了要去实现具体功能的时候,无须关注那么具体的实现细节,只需要站在更高的层面去分析整体的流程。

有兴趣探究某一部分实现细节的同学,可以查阅 webpack 源码,从 webpack 基础流程入手:Compiler Hooks。

这里提供的是 4.x 版本的源码 master 分支的链接地址,webpack 的源码相对难懂,如果是想要学习 bundler 的整个工作流程,可以考虑看阅读 rollup 的源码,可读性相对会好很多。

webpack 的源码

webpack 主要的构建处理方法都在 Compilation 中,我们要了解 loader 和 plugin 的机制,就要深入 Compilation 这一部分的内容。

Compilation 的实现也是比较复杂的,lib/Compilation.js 单个文件代码就有近 2000 行之多,我们挑关键的几个部分来介绍一下。

addEntry 和 _addModuleChain

addEntry 这个方法顾名思义,用于把配置的入口加入到构建的任务中去,当解析好 webpack 配置,准备好开始构建时,便会执行 addEntry 方法,而 addEntry 会调用 _addModuleChain 来为入口文件(入口文件这个时候等同于第一个依赖)创建一个对应的 Module 实例。

_addModuleChain 方法会根据入口文件这第一个依赖的类型创建一个 moduleFactory,然后再使用这个 moduleFactory 给入口文件创建一个 Module 实例,这个 Module 实例用来管理后续这个入口构建的相关数据信息,关于 Module 类的具体实现可以参考这个源码:lib/Module.js,这个是个基础类,大部分我们构建时使用的代码模块的 Module 实例是 lib/NormalModule.js 这个类创建的。

我们介绍 addEntry 主要是为了寻找整个构建的起点,让这一切有迹可循,后续的深入可以从这个点出发。

buildModule

当一个 Module 实例被创建后,比较重要的一步是执行 compilation.buildModule 这个方法,这个方法主要会调用 Module 实例的 build 方法,这个方法主要就是创建 Module 实例需要的一些东西,对我们梳理流程来说,这里边最重要的部分就是调用自身的 runLoaders 方法。

runLoaders 这个方法是 webpack 依赖的这个类库实现的:loader-runner,这个方法也比较容易理解,就是执行对应的 loaders,将代码源码内容一一交由配置中指定的 loader 处理后,再把处理的结果保存起来。

我们之前介绍过,webpack 的 loader 就是转换器,loader 就是在这个时候发挥作用的,至于 loader 执行的细节,有兴趣深入的同学可以去了解 loader-runner 的实现。

上述提到的 Module 实例的 build 方法在执行完对应的 loader,处理完模块代码自身的转换后,还有相当重要的一步是调用 Parser 的实例来解析自身依赖的模块,解析后的结果存放在 module.dependencies 中,首先保存的是依赖的路径,后续会经由 compilation.processModuleDependencies 方法,再来处理各个依赖模块,递归地去建立整个依赖关系树。

Compilation 的钩子

我们前边提到了 webpack 会使用 tapable 给整个构建流程中的各个步骤定义钩子,用于注册事件,然后在特定的步骤执行时触发相应的事件,注册的事件函数便可以调整构建时的上下文数据,或者做额外的处理工作,这就是 webpack 的 plugin 机制。

在 webpack 执行入口处 lib/webpack.js 有这么一段代码:

if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    if (typeof plugin === "function") {
      plugin.call(compiler, compiler);
    } else {
      plugin.apply(compiler);
    }
  }
}

这个 plugin 的 apply 方法就是用来给 compiler 实例注册事件钩子函数的,而 compiler 的一些事件钩子中可以获得 compilation 实例的引用,通过引用又可以给 compilation 实例注册事件函数,以此类推,便可以将 plugin 的能力覆盖到整个 webpack 构建过程。

而关于这些事件函数的名称和定义可以查看官方的文档:compiler 的事件钩子 和 compilation 的事件钩子。

后续有章节会介绍如何编写 webpack plugin,可以将两部分的内容结合一下,来帮助理解 webpack plugin 的执行机制。

产出构建结果

最后还有一个部分,即用 Template 产出最终构建结果的代码内容,这一部分不作详细介绍了,仅留下一些线索,供有兴趣继续深入的同学使用:

  • Template 基础类:lib/Template.js
  • 常用的主要 Template 类:lib/MainTemplate.js
  • Compilation 中产出构建结果的代码:compilation.createChunkAssets

这一部分内容的介绍就到这里了,对此部分内容有兴趣继续深入探索的同学,建议使用断点调试的方式,结合笔者介绍的这些内容,大致走一遍 webpack 的构建流程,会对这一部分的内容印象更加深刻,同时也可以通过断点更有针对性地了解某一部分的细节处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1198936.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Doris学习--1、Doris简介、操作Doris、Doris架构(数据模型)

星光下的赶路人star的个人主页 心之所向,剑之所往 文章目录 1、Doris简介1.1 快速开始1.2 安装配置1.2.1 应知前提1.2.2 配置Doris1.2.2.0 配置前提1.2.2.1 配置FE(Frontend)1.2.2.2 启动FE1.2.2.3 连接FE1.2.2.4 停止FE1.2.2.5 配置BE&#…

【函数讲解】pygmo中的函数 fast_non_dominated_sorting() + 利用支配关系,学习一个SVM分类器,将解分为两类

这个函数是用来执行非支配排序的,可以分层构建Pareto,并返回每一层的解以及每个解支配其他解的索引、解被其他解支配的次数、解所在的非支配层级。这个函数对这些解进行非支配排序,并返回四个数组:ndf, dl, dc, 和 ndr。 ndf (Non…

2.3 CE修改器:浮点数扫描

本关需要使用 Cheat Engine 工具对浮点数进行扫描,完成修改任务。浮点数是一种带有小数点的数值,通过“浮点数”扫描方式进行修改。本关中,健康值为单精度浮点数,弹药值为双精度浮点数,需要将这两项数值都修改为 5000 …

InSAR形变监测方法与研究进展(朱建军,中南大学)

文章目录 摘要引言InSARInSAR原理SAR卫星 InSAR监测技术D-InSARMT-InSARPS-InSARSBAS-InSARDS-InSAR(Distributed Scatterer InSAR)MAI(Multi-Aperture InSAR, 多孔径InSAR) InSAR形变监测应用与发展城市沉降监测矿山形变监测地震…

深度探究深度学习常见数据类型INT8 FP32 FP16的区别即优缺点

定点和浮点都是数值的表示(representation),它们区别在于,将整数(integer)部分和小数(fractional)部分分开的点,点在哪里。定点保留特定位数整数和小数,而浮点…

用互联网思维打造物流网(别人笑我太疯癫,我把自己当成仙)

引言 最近在写网络相关文章,在类比互联网与物流网时发现他们有很多相似之处。 互联网传输的是数据,物流网传输的是物品,功能相似,都是用于传输。 互联网在传输数据时,通过路由选择最佳传输路线;物流在运…

微信小程序数据交互和缓存

目录 前言: 数据交互 1. 发起网络请求 2. WebSocket 2.1实时数据库 3. 微信支付 数据缓存 1. 页面级缓存 2. 内存级缓存 3. 数据缓存策略 优化用户体验 总结 前言: 在开发微信小程序时,数据交互和缓存是非常重要的方面。本文将介…

【已解决】ModuleNotFoundError: No module named ‘matplotlib‘

问题描述 Traceback (most recent call last): File "/home/visionx/nickle/temp/SimCLR/linear_evaluation.py", line 207, in <module> import matplotlib.pyplot as plt ModuleNotFoundError: No module named matplotlib 解决办法 pip install matp…

2020年-2022年聚合支付牌照机构评级结果分析,D/E有所增加

本文首发于移动支付网&#xff0c;标题“聚合支付机构最新评级结果公布&#xff0c;A-、B级别机构减少”&#xff0c;该文是基于其内容进行了数据修订及部分内容优化而成文。 11月7日&#xff0c;中国支付清算协会发布2022年度收单外包服务机构评级等级结果。本次评级工作&…

Vert.x学习笔记-什么是Verticle

什么是Verticle Verticle是Vert.x应用中的基本编程单元&#xff0c;类似于Java中的Servlet、Pojo Bean或Akka中的Actor。它可以使用不同的编程语言实现&#xff0c;并且这些由不同编程语言实现的Verticle可以封装到一个模块中&#xff0c;进而部署到一个Vert.x应用中。Verticl…

海康威视嵌入式软件一面(技术面)

海康威视技术面试大部分都是基础问题和牛客上的问题&#xff0c;最后还有手撕代码部分也是牛客原题&#xff0c;总体中等偏难。 一、问答题 1.什么是野指针&#xff0c;野指针如何形成 【C语言基础】野指针与空指针_野指针和空指针-CSDN博客 2.const和static作用和区别 sta…

Java算法(七):随机产生验证码 前后端验证码比对处理 实战思路步骤

Java算法&#xff08;七&#xff09; 随机产生验证码 package com.liujintao.random;import java.util.Random; import java.util.Scanner;public class RandomNumber {/*** 该函数调用验证码所有的函数&#xff0c;完成验证码模块功能开发* param args*/public static void …

2.1 CE修改器:精确数值扫描

本关是CE修改器的第一关&#xff0c;用户需要通过 Cheat Engine 工具完成精确扫描值。在这个练习中&#xff0c;需要将一个特定的数值&#xff08;健康值&#xff09;改变为 1000。首先&#xff0c;要确保数值类型设置正确&#xff0c;默认的是2字节或4字节。接着&#xff0c;选…

Tkinter,一个轻量级的Python GUI库

欢迎关注作者微信公众号&#xff1a;愤怒的it男 Tkinter&#xff08;即 tk interface&#xff0c;简称“Tk”&#xff09;本质上是对Tcl/Tk软件包的Python接口封装&#xff0c;属于Python自带的标准库&#xff0c;安装好Python后可以直接使用Tkinter库而无须另行安装。Tkinter库…

亲身体验告诉你:亚马逊云科技海外服务器是否值得一试?

前言 在当今数字化时代&#xff0c;云计算已经成为企业和个人发展的重要支撑。亚马逊云科技作为全球领先的云计算服务提供商&#xff0c;其海外服务器备受瞩目。然而&#xff0c;对于一些用户来说&#xff0c;是否值得一试亚马逊云科技的海外服务器仍然是一个疑问。本文将通过亲…

深入了解SpringMvc接收数据

目录 一、访问路径&#xff08;RequestMapping&#xff09; 1.1 访问路径注解作用域 1.5 路径精准&#xff08;模糊&#xff09;匹配 1.2 访问路径限制请求方式 1.3 进阶访问路径请求注解 1.4 与WebServlet的区别 二、接收请求数据 2.1 请求param参数 2.2 请求路径参数 2.3 请求…

MySQL数据库实验记录

输入密码 显示数据库 mysql命令以分号;结束 创建数据库 建表 写错了就会报错 没选数据库也会报错

GEE:计算有效像素占比(统计有效像素数量、像素总数)

作者:CSDN @ _养乐多_ 在GEE中进行遥感数据处理的时候,经常会由于去云,导致影像出现空洞,只有部分像素可用,或者在进行特殊处理时,只对有效像素进行处理,但是我们不知道有效像素数量和占比,无法对结果做出准确的分析。这个时候就需要统计有效像素数量占比。 本文记录…

【论文笔记】Denoising Diffusion Probabilistic Models

Pre Knowledge 1.条件概率的一般形式 P ( A , B ) P ( B ∣ A ) P ( A ) P(A,B)P(B|A)P(A) P(A,B)P(B∣A)P(A) P ( A , B , C ) P ( C ∣ B , A ) P ( B , A ) P ( C ∣ B , A ) P ( B ∣ A ) P ( A ) P(A,B,C)P(C|B,A)P(B,A)P(C|B,A)P(B|A)P(A) P(A,B,C)P(C∣B,A)P(B,A)P…

Python之函数进阶-nonlocal和LEGB

Python之函数进阶-nonlocal和LEGB nonlocal语句 nonlocal:将变量标记为不在本地作用域定义&#xff0c;而是在上级的某一级局部作用域中定义&#xff0c;但不能是全局作用域中定义。 函数的销毁 定义一个函数就是生成一个函数对象&#xff0c;函数名指向的就是函数对象。可…