普通视图

发现新文章,点击刷新页面。
昨天以前首页

Webpack 打包未使用组件的原因

作者 糯糯机器
2025年4月15日 17:30

在项目开发过程中,近期对项目进行了双页面配置,针对两个不同页面分别单独配置了路由。然而,在单独启动新页面(该页面模块数量较少)时,却发现启动耗时较长。经过深入排查,最终确定是以下这行代码导致了该问题:

return () => import(`@/pages/PRD/views/${realPath}`);

Webpack 在遇到import()语法时,会依据给定路径将所有符合条件的模块进行打包。由于上述导入代码的最后一个部分是变量,对于 Webpack 而言,这等同于@/pages/PRD/views/**/*,因此 Webpack 会将 views 目录下的所有模块及其相关依赖一同打包。

为深入探究 Webpack 对import的处理机制,我创建了一个项目,并针对不同情况进行打包测试,最终得出以下结论:

// `import`的参数不能是完全动态的语句,例如`import(util)`是不被允许的,至少需要包含部分模块路径信息,以便Webpack确定打包范围。
const component = () => {
  const path = "xxx";
  return import(path); 
}
// 这种导入方式会将xxx目录下的所有组件和js文件都进行打包。
const componentB = (name) => {
  const component = "component" + name;
  return import(`./xxx/${component}`); 
}
// 这种导入方式会将xxx目录下以component开头的文件进行打包。
// 如果xxx目录下存在/component*/这样的文件夹,那么文件夹内的所有组件都会被打包。
const componentC = (name) => {
  const component = 
  return import(`./xxx/component${name}`); 
}
// 较为合适的动态引入方式
const componentD = (name) => {
  const component = 
  return import(`./xxx/component${name}.vue`); 
}

Webpack 解析 import 注释指令

在 Webpack 中,import()函数的注释指令是一种特殊语法,用于控制 Webpack 对动态导入模块的处理方式,主要包含以下几种:

webpackChunkName

  • 功能:该指令用于手动指定 Webpack 拆分代码时生成的代码块(chunk)名称。合理命名代码块,有助于在打包结果中清晰区分不同功能模块的代码块,对缓存管理、调试以及性能分析等工作都非常有利。
  • 示例

    import(/* webpackChunkName: "user - module - chunk" */ './userModule.js');
    

    在上述代码中,Webpack 会将./userModule.js及其依赖模块打包到名为user - module - chunk的代码块中。从 Webpack 2.6.0 版本开始,还支持使用占位符,例如(index)会使代码块名称以递增数字进行命名,(request)则会使用实际解析的文件名。示例如下:

    // 假设导入多个模块,会生成类似userModule1、userModule2等命名的代码块
    import(/* webpackChunkName: "userModule(index)" */ './userModule.js'); 
    

webpackMode

  • 功能:该指令用于指定 Webpack 解析动态导入的模式。在不同模式下,Webpack 对动态导入模块的处理逻辑以及生成代码块的方式会有所不同。
  • 模式类型及示例
  • lazy:这是默认模式。在这种模式下,Webpack 会为每个import()调用单独生成可延迟加载的代码块。也就是说,每次遇到import(),Webpack 都会创建一个新的独立代码块,在运行时按需加载。例如:

    import(/* webpackMode: "lazy" */ './module1.js');
    import(/* webpackMode: "lazy" */ './module2.js');
    

    上述代码会分别生成两个独立的代码块,用于加载module1.js和module2.js。

  • lazy - once:此模式下,Webpack 会生成单个可延迟加载的代码块,该代码块能够满足多个import()调用的需求。适用于一些动态导入语句,比如import(./locales/${language}.json),可能会根据不同的language值请求多个模块路径的情况。首次调用import()时获取代码块,后续调用则复用该代码块。示例如下:

    // 多个动态导入可能共用一个代码块
    import(/* webpackMode: "lazy - once" */ `./locales/${language}.json`); 
    import(/* webpackMode: "lazy - once" */ `./locales/${anotherLanguage}.json`); 
    
  • eager:在该模式下,Webpack 不会生成额外的代码块,而是将被导入的模块直接引入当前代码块,并返回已解析状态的Promise。与静态导入不同的是,在import()调用完成前,模块不会执行。例如:

    import(/* webpackMode: "eager" */ './eagerModule.js').then((module) => {
      module.someFunction();
    });
    

【从零打造视觉盛宴・风格篇】私藏技巧:如何把普通场景做成高级感爆棚的创意作品?

作者 Mapmost
2025年4月14日 18:40

宫崎骏的作品以细腻笔触与梦幻色彩闻名,让人一眼难忘。随着三维渲染技术的突破,数字模型也能渲染出手绘的温度,呈现出柔和轮廓与流动光影,制作出类似手绘作品的视觉感。

Mapmost SDK for WebGL 渲染图

其实,这种效果是依赖三维软件的后期渲染能力达到的。后期渲染可以理解为,通过色彩、光影、特效等手段优化画面,赋予其生动细节与艺术风格的一种模型渲染方式。常见的后期渲染参数包括色调、曝光度、对比度、描边、辉光、环境雾等。

接下来,小编将利用Mapmost SDK for WebGL中的后期渲染参数,制作一个动漫风格的场景。通过调整这些参数,为场景赋予独特的视觉风格。

_一、_控制太阳光

太阳光是场景的主要照明光源,也是3D场景中的“气氛组担当”。太阳光颜色的变化能够带来不同的氛围效果。

动图封面

Mapmost SDK for WebGL 渲染图

_二、_调整曝光度

曝光度和相机曝光原理相似,主要决定画面亮部的明暗。灵活调整曝光度,可以让画面更具表现力,更容易抓住观众的目光。

动图封面

Mapmost SDK for WebGL 渲染图

_三、_设置HDR

HDR是高动态范围成像(英语:High Dynamic Range Imaging,简称HDRI或HDR),通常是一张环境图片,它就像场景的 “细节侦察兵”,能让画面亮部和暗部的细节清晰可见。如果HDR自身存在光源点,需要注意太阳光的所在位置。如图所示,环境光源点方向与太阳光方向混乱,破坏了画面的协调感。

Mapmost SDK for WebGL渲染图

_四、_优化对比度

对比度是一个可以让画面暗处更暗,亮处更亮的双向调节参数。通过对比度调整,可以让模型轮廓很清晰,立体感更强。

动图封面

Mapmost SDK for WebGL渲染图

_五、_控制饱和度

饱和度是决定画面鲜艳程度的参数。在写实的场景里,饱和度通常比较低。而风格化场景里,可以尝试调整饱和度,来营造特殊的视觉效果。

动图封面

Mapmost SDK for WebGL渲染图

_六、_显示描边效果

描边是勾勒模型的边界。在数字孪生场景中,常被用来做物体选中、高亮等功能,属于较为常用的表达方式。但是,当在风格化场景里开启描边效果时,则能够达到增添艺术感,塑造手绘风格的效果。

Mapmost SDK for WebGL渲染图

_七、_增加LUT

LUT 是显示查找表(Look-Up-Table), 简而言之就是3D 场景的“一键调色工具”,可以像美图秀秀的滤镜一样给画面调整色彩,一键切换出不同的色彩风格。

动图封面

Mapmost SDK for WebGL渲染图

通过以上七个后期效果的巧妙组合与精细调节,我们能够快速达到图中的渲染效果,让模型呈现出独特的手绘风格。

Mapmost SDK for WebGL渲染图

除了WebGL,另一款SDK -Mapmost SDK for UE5凭借引擎强大的渲染能力,可实现超真实视觉呈现(如PBR材质、全局光照、动态阴影及天气系统),并灵活适配多种风格化效果(如风格化渲染、昼夜切换)效果,为城市全域管理及智能化决策提供支持。

Mapmost SDK for UE 渲染图

如果您有项目开发需求,欢迎随时与我们联系。我们将为您提供专业的技术支持,助力您实现理想的场景效果呈现。点击此处,跳转官网体验Mapmost SDK for WebGL 与 Mapmost SDK for UE 产品!

关注 Mapmost,持续更新 GIS、三维美术、计算机技术干货

Mapmost 是一套以三维地图和时空计算为特色的数字孪生底座平台,包含了空间数据管理工具(Studio)、应用开发工具(SDK)、应用创作工具(Alpha)。平台能力已覆盖城市时空数据的集成、多源数据资源的发布管理,以及数字孪生应用开发工具链,满足企业开发者用户快速搭建数字孪生场景的切实需求,助力实现行业领先。

欢迎进入官网体验使用:Mapmost——让人与机器联合创作成为新常态

Threejs 奇幻几何体:边缘、线框、包围盒大冒险

作者 伶俜monster
2025年4月14日 15:55

几何体

边缘几何体

可以作为一个辅助对象来查看几何体的边缘。

gltfLoader.load("./models/building.gltf", (gltf) => {
  console.log(gltf);
  //隐藏后就可以直接看到线框
  // scene.add(gltf.scene);

  //这个模型是由多个几何体组成的,所以需要遍历
  gltf.scene.traverse((child) => {
    if (child.isMesh) {
      //获取几何体
      const geometry = child.geometry;

      // //获取边缘
      const edges = new THREE.EdgesGeometry(geometry);

      //创建线条材质
      const lineMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });

      //创建线条
      const line = new THREE.LineSegments(edges, lineMaterial);
      //将线条添加到场景中
      scene.add(line);
    }
  });
});

边缘几何体1.png

EdgesGeometry 构造函数用于创建边缘几何体,接受一个几何体对象作为参数。

LineBasicMaterial 构造函数用于创建线条材质,接受一个对象作为参数。

LineSegments 构造函数用于创建线条,接受两个参数:

  • geometry: 线条几何体。

  • material: 线条材质。

traverse 方法用于遍历场景中的所有对象,接受一个回调函数作为参数。

边缘几何体2.png

线框几何体

let edges = new THREE.WireframeGeometry(geometry);

WireframeGeometry 构造函数用于创建线框几何体,接受一个几何体对象作为参数。

线框几何体.png

模型解压

在 Three.js 中,模型解压是指将压缩的模型文件解压缩为 Three.js 可以直接使用的模型对象的过程。通常,模型文件在压缩后可以减小文件大小,加快加载速度,但是解压缩过程需要消耗一定的计算资源。

正如上面加载的 building.gltf 模型是压缩过的,如果正常加载会报错,所以需要解压。

解压器.png

DRACOLoader

DRACOLoader 是 Three.js 提供的一个用于加载和解压缩 Draco 压缩格式的模型文件的加载器。Draco 是一个开源的 3D 图形压缩库,可以有效地减少 3D 模型的文件大小,加快加载速度,并保持较高的图形质量。

使用步骤:

  1. 引入 DRACOLoader
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
  1. 创建 DRACOLoader 实例
const dracoLoader = new DRACOLoader();
  1. 设置 DRACOLoader 的解析器路径
dracoLoader.setDecoderPath("./draco/");

注意: 解析器路径是相对于当前 HTML 文件的路径,所以需要把 three/examples/jsm/libs/draco 文件夹拷贝到项目public中。

  1. 设置加载器 draco 解码器
gltfLoader.setDRACOLoader(dracoLoader);

包围盒

包围盒(Bounding Box)是一个用于描述物体体积的几何体,用于计算物体的位置、大小和碰撞等属性。在三维图形学中,包围盒通常用于加速物体的碰撞检测和渲染。

Box3

objLoader.load("./models/1.obj", (obj) => {
  scene.add(obj);
  obj.children[0].material.map = new THREE.TextureLoader().load("./models/nezha.png");

  //获取模型,可以直接根据数据找,也可以使用getObjectByName方法
  // const nezha = obj.children[0]
  const nezha = obj.getObjectByName("texture_v100");
  //获取几何体
  const nazhaGeometry = nezha.geometry;
  //计算包围盒
  nazhaGeometry.computeBoundingBox();
  //获取包围盒
  const nazhaBox = nazhaGeometry.boundingBox;
  //创建包围盒辅助线
  const nazhaBoxHelper = new THREE.Box3Helper(nazhaBox, 0xff0000);
  scene.add(nazhaBoxHelper);
});

getObjectByName 方法用于获取指定名称的对象,返回值为对象本身。

.computeBoundingBox() 方法用于计算几何体的包围盒,返回值为包围盒对象。

.boundingBox 属性用于获取几何体的包围盒,返回值为包围盒对象。

Box3Helper 构造函数用于创建包围盒辅助线,接受两个参数:

  • box: 要显示的包围盒对象。

  • color: 辅助线的颜色。

包围盒.png

包围盒数据.png

因为生成的包围盒是一个立方体,所以可以直接通过 min 和 max 属性获取立方体的最小和最大坐标。可快速生成包围盒。

Sphere

objLoader.load("./models/1.obj", (obj) => {
  scene.add(obj);
  obj.children[0].material.map = new THREE.TextureLoader().load("./models/nezha.png");

  const nezha = obj.getObjectByName("texture_v100");
  //获取几何体
  const nazhaGeometry = nezha.geometry;

  //计算包围球
  nazhaGeometry.computeBoundingSphere();
  //获取包围球
  const nazhaSphere = nazhaGeometry.boundingSphere;
  //创建包围球
  const sphereGeometry = new THREE.SphereGeometry(nazhaSphere.radius, 16, 16);
  const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  sphere.position.copy(nazhaSphere.center);
  scene.add(sphere);
});

.computeBoundingSphere() 方法用于计算几何体的包围球,返回值为包围球对象。

.boundingSphere 属性用于获取几何体的包围球,返回值为包围球对象。

包围球对象.png

包围球.png

多物体包围

//创建球
const sphere1 = new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 32, 32),
  new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
sphere1.position.set(2, 0, 0);
scene.add(sphere1);
const sphere2 = new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 32, 32),
  new THREE.MeshBasicMaterial({ color: 0x00ff00 })
);
scene.add(sphere2);
const sphere3 = new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 32, 32),
  new THREE.MeshBasicMaterial({ color: 0x0000ff })
);
sphere3.position.set(-2, 0, 0);
scene.add(sphere3);

let box = new THREE.Box3();
let sphereArr = [sphere1, sphere2, sphere3];
for (let i = 0; i < sphereArr.length; i++) {
  //计算包围盒
  sphereArr[i].geometry.computeBoundingBox();
  let box3 = sphereArr[i].geometry.boundingBox;
  //更新世界矩阵
  sphereArr[i].updateMatrixWorld(true, true);
  //应用矩阵
  box3.applyMatrix4(sphereArr[i].matrixWorld);
  //获取包围盒
  box.union(box3);
}
const boxHelper = new THREE.Box3Helper(box, 0xff0000);
scene.add(boxHelper);

.union() 方法用于合并两个包围盒,返回值为合并后的包围盒对象。

注意: 这里涉及到物体的合并,那么需要先更新世界矩阵,再应用矩阵。

//简易方法
for (let i = 0; i < sphereArr.length; i++) {
  //第一种
  sphereArr[i].geometry.computeBoundingBox();
  let box3 = sphereArr[i].geometry.boundingBox;
  sphereArr[i].updateMatrixWorld(true, true);
  box3.applyMatrix4(sphereArr[i].matrixWorld);

  //第二种简易方法
  let box3 = new THREE.Box3().setFromObject(sphereArr[i]);

  box.union(box3);
}

.setFromObject() 方法用于从物体中获取包围盒,返回值为包围盒对象。

多物体包围.png

扩展

.getCenter() 方法用于获取包围盒或包围球的中心点,返回值为向量对象。

Geometry.center() 几何体居中,返回值为几何体对象。

.updateWorldMatrix() 方法用于更新几何体的世界矩阵,返回值为几何体对象。

.applyMatrix4(matrix) 方法用于将指定的矩阵应用于几何体的顶点,返回值为几何体对象。

如果存在物体和包围盒的变换,那么需要先更新世界矩阵,再应用矩阵。

//更新世界矩阵
nazhaGeometry.updateWorldMatrix(true, true);
//应用矩阵
nazhaGeometry.applyMatrix4(nazha.matrixWorld);

[书洞笔记](mp.weixin.qq.com/s/8cyl1tzrx…

拆包的艺术:Webpack SplitChunksPlugin 执行流程全流程揭秘 🤩🤩🤩

作者 Moment
2025年4月13日 11:28

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777

SplitChunks 原理的实现,它的源码位置在 webpack/lib/optimize/SplitChunksPlugin.js,进入到源码,我们首先看到 SplitChunks 的 class 定义:

module.exports = class SplitChunksPlugin {
    constructor(options = {}) {
        const defaultSizeTypes = options.defaultSizeTypes || [            "javascript",            "unknown"        ];
        const fallbackCacheGroup = options.fallbackCacheGroup || {};
        const minSize = normalizeSizes(options.minSize, defaultSizeTypes);
        const minSizeReduction = normalizeSizes(
            options.minSizeReduction,
            defaultSizeTypes
        );
        const maxSize = normalizeSizes(options.maxSize, defaultSizeTypes);

        /** @type {SplitChunksOptions} */
        //options的初始化逻辑,概括为我们的配置优先,如果没有则采用默认规则里的相关项
        this.options = {
            chunksFilter: normalizeChunksFilter(options.chunks || "all"),
            defaultSizeTypes,
            minSize,
            minSizeReduction,
            minRemainingSize: mergeSizes(
                normalizeSizes(options.minRemainingSize, defaultSizeTypes),
                minSize
            ),
            enforceSizeThreshold: normalizeSizes(
                options.enforceSizeThreshold,
                defaultSizeTypes
            ),
            maxAsyncSize: mergeSizes(
                normalizeSizes(options.maxAsyncSize, defaultSizeTypes),
                maxSize
            ),
            maxInitialSize: mergeSizes(
                normalizeSizes(options.maxInitialSize, defaultSizeTypes),
                maxSize
            ),
            minChunks: options.minChunks || 1,
            maxAsyncRequests: options.maxAsyncRequests || 1,
            maxInitialRequests: options.maxInitialRequests || 1,
            hidePathInfo: options.hidePathInfo || false,
            filename: options.filename || undefined,
            getCacheGroups: normalizeCacheGroups(
                options.cacheGroups,
                defaultSizeTypes
            ),
            getName: options.name ? normalizeName(options.name) : defaultGetName,//这里就是我们提到的如果name为true则使用chunks配置的name
            automaticNameDelimiter: options.automaticNameDelimiter,
            usedExports: options.usedExports,
            fallbackCacheGroup: {
                chunksFilter: normalizeChunksFilter(
                    fallbackCacheGroup.chunks || options.chunks || "all"
                ),
                minSize: mergeSizes(
                    normalizeSizes(fallbackCacheGroup.minSize, defaultSizeTypes),
                    minSize
                ),
                maxAsyncSize: mergeSizes(
                    normalizeSizes(fallbackCacheGroup.maxAsyncSize, defaultSizeTypes),
                    normalizeSizes(fallbackCacheGroup.maxSize, defaultSizeTypes),
                    normalizeSizes(options.maxAsyncSize, defaultSizeTypes),
                    normalizeSizes(options.maxSize, defaultSizeTypes)
                ),
                maxInitialSize: mergeSizes(
                    normalizeSizes(fallbackCacheGroup.maxInitialSize, defaultSizeTypes),
                    normalizeSizes(fallbackCacheGroup.maxSize, defaultSizeTypes),
                    normalizeSizes(options.maxInitialSize, defaultSizeTypes),
                    normalizeSizes(options.maxSize, defaultSizeTypes)
                ),
                automaticNameDelimiter:
                    fallbackCacheGroup.automaticNameDelimiter ||
                    options.automaticNameDelimiter ||
                    "~"
            }
        };

        /** @type {WeakMap<CacheGroupSource, CacheGroup>} */
        //cacheGroup的数据结构是WeakMap,这样做的目的是缓存每一个CacheGroup后续使用
        this._cacheGroupCache = new WeakMap();
    }

准备阶段

SplitChunksPlugin 的执行过程中,准备阶段的核心工作是构建和维护 chunksInfoMap,它存储了所有关于代码分割的关键信息。可以理解为这是一个集中管理分割规则的数据结构,包含了每个 chunk 的大小、模块数量等信息。这些数据为后续的 minSizeminChunks 条件提供支持,chunksInfoMap 会不断更新,直到最终生成 results,这些 results 就是我们拆分后的最终 chunk 文件。

在这过程中,最重要的操作之一是 addModuleToChunksInfoMap,它负责将模块的分割信息添加到 chunksInfoMap 中。此外,多个辅助方法也在起作用,如处理 chunksSetchunksKey 等,这些方法帮助有效地管理和组织不同的 chunk 组合。

chunksKey 设置

在 SplitChunksPlugin 中,每个 chunk 都会被赋予一个唯一的标识符(即唯一索引)。这个过程通过以下方式实现:

const chunkIndexMap = new Map();
const ZERO = BigInt("0");
const ONE = BigInt("1");
const START = ONE << BigInt("31");
let index = START;
for (const chunk of chunks) {
  chunkIndexMap.set(chunk, index | BigInt((Math.random() * 0x7fffffff) | 0));
  index = index << ONE;
}

chunkIndexMap 是一个 Map,用于存储每个 chunk 和其对应的唯一索引。这个索引是通过以下几步生成的:

  1. BigInt 类型:使用 BigInt 来确保可以为每个 chunk 分配一个唯一且大范围的标识符,避免数值溢出。

  2. START 常量:START 是一个常量,表示索引的起始值,ONE << BigInt("31") 将 1 向左移 31 位,得到一个大整数作为起点。

  3. 递增的索引:index = index << ONE 每次将索引左移,这样生成的索引每次都会变大,确保每个 chunk 都有一个唯一的标识符。

通过 index | BigInt((Math.random() * 0x7fffffff) | 0) 这行代码,给每个 chunk 分配一个 随机数,避免出现重复的索引。这样,即使是同一个构建过程中,多个 chunk 也能拥有不同的索引。

接下来是生成 chunkKey,它是一个唯一的标识符,用于表示一组具有相同依赖关系的 chunks。它通过以下函数生成:

const getKey = (chunks) => {
  const iterator = chunks[Symbol.iterator]();
  let result = iterator.next();
  if (result.done) return ZERO;
  const first = result.value;
  result = iterator.next();
  if (result.done) return first;
  let key = chunkIndexMap.get(first) | chunkIndexMap.get(result.value);
  while (!(result = iterator.next()).done) {
    const raw = chunkIndexMap.get(result.value);
    key = key ^ raw;
  }
  return key;
};

迭代 chunks 集合:getKey 函数接受一个包含多个 chunks 的集合作为参数,并通过迭代器遍历这些 chunks。

生成 key:每个 chunk 的索引值来自于之前生成的 chunkIndexMap,通过按位操作(|^)将每个 chunk 的索引值合并成一个唯一的 key:

  1. 按位 |(或):用于合并两个 chunk 的索引,确保即使是同一组 chunk 也会生成唯一的 key。

  2. 按位 ^(异或):用于进一步结合多个 chunk 的索引,确保如果有更多的 chunk,它们之间的关系也能通过 key 表示出来。

最终 getKey 返回的 key 唯一地表示这组 chunks,并且这个 key 对于具有相同依赖的 chunks 来说是相同的。

chunkKeysSplitChunksPlugin 中的作用是为一组具有相同依赖关系的 chunks 生成唯一标识符。通过为每个 chunk 分配唯一的索引,并使用位运算合并这些索引,chunkKeys 确保共享依赖的 chunk 可以被正确分组。这样,当多个 chunk 共享相同的模块时,它们会生成相同的 chunkKey,使得插件能够识别出这些 chunk 之间的关系,并优化它们的拆分策略。chunkKeys 使得 Webpack 能够高效地管理和组织 chunks,避免重复的模块打包,并确保共享模块能够提取到独立的 chunk,从而提升性能和缓存命中率。

处理应用次数

count 在这里表示 chunksSet 集合中包含的 chunk 数量。

这个函数 groupChunkSetsByCount 的作用是将 chunk 集合按照其中包含的 chunk 数量进行分组。具体来说:

  1. 它接收一个 chunk 集合的迭代器 chunkSets 作为参数

  2. 创建一个 Map chunkSetsByCount,键是 chunk 数量,值是包含相同数量 chunk 的集合数组

  3. 遍历每个 chunksSet,获取其大小(即包含的 chunk 数量)

  4. chunksSet 添加到对应数量的数组中

chunkSets 是通过 getChunkSetsInGraph()getExportsChunkSetsInGraph() 函数获取的,实际上就是我们项目中编写的模块。这些函数会分析整个模块依赖图,找出模块与 chunks 之间的关系。

这种分组方式在后续处理中很有用,特别是在检查子集关系时。因为只有较小集合才可能是较大集合的子集,所以按照大小分组可以减少比较次数,提高性能。

例如,如果有一些 chunk 集合,分别包含 2、3、2、5、3 个 chunk,这个函数会将它们分组为:

  • 键为 2 的数组:包含 2 个 chunk 的集合

  • 键为 3 的数组:包含 3 个 chunk 的集合

  • 键为 5 的数组:包含 5 个 chunk 的集合

下面这段代码就是具体处理合并同 key 的 chunk 并处理子集的逻辑:

const createGetCombinations = (
  chunkSets,
  singleChunkSets,
  chunkSetsByCount
) => {
  /** @type {Map<bigint | Chunk, (Set<Chunk> | Chunk)[]>} */
  const combinationsCache = new Map();

  return (key) => {
    const cacheEntry = combinationsCache.get(key);
    if (cacheEntry !== undefined) return cacheEntry;
    if (key instanceof Chunk) {
      const result = [key];
      combinationsCache.set(key, result);
      return result;
    }
    const chunksSet =
      /** @type {Set<Chunk>} */
      (chunkSets.get(key));
    /** @type {(Set<Chunk> | Chunk)[]} */
    const array = [chunksSet];
    for (const [count, setArray] of chunkSetsByCount) {
      // "equal" is not needed because they would have been merge in the first step
      if (count < chunksSet.size) {
        for (const set of setArray) {
          if (isSubset(chunksSet, set)) {
            array.push(set);
          }
        }
      }
    }
    for (const chunk of singleChunkSets) {
      if (chunksSet.has(chunk)) {
        array.push(chunk);
      }
    }
    combinationsCache.set(key, array);
    return array;
  };
};

createGetCombinations 函数创建一个高效的组合查找器,它利用 groupChunkSetsByCount 预先分组的数据结构,快速定位并返回特定 chunk 集合的所有可能子集组合。通过智能缓存机制避免重复计算,且只比较小于当前集合大小的其他集合,显著减少了比较操作次数。这个函数是 SplitChunksPlugin 核心算法的关键部分,它帮助 webpack 在复杂项目中快速找到最佳代码分割方案,确定哪些模块组合应该被提取到共享 chunk 中,从而优化最终打包结果的体积和加载性能。

拆分缓存组

/**
 * @param {CacheGroup} cacheGroup the current cache group
 * @param {number} cacheGroupIndex the index of the cache group of ordering
 * @param {Chunk[]} selectedChunks chunks selected for this module
 * @param {bigint | Chunk} selectedChunksKey a key of selectedChunks
 * @param {Module} module the current module
 * @returns {void}
 */
const addModuleToChunksInfoMap = (
  cacheGroup,
  cacheGroupIndex,
  selectedChunks,
  selectedChunksKey,
  module
) => {
  // Break if minimum number of chunks is not reached
  if (selectedChunks.length < /** @type {number} */ (cacheGroup.minChunks))
    return;
  // Determine name for split chunk

  const name =
    /** @type {string} */
    (
      /** @type {GetName} */
      (cacheGroup.getName)(module, selectedChunks, cacheGroup.key)
    );
  // Check if the name is ok
  const existingChunk = compilation.namedChunks.get(name);
  if (existingChunk) {
    const parentValidationKey = `${name}|${
      typeof selectedChunksKey === "bigint"
        ? selectedChunksKey
        : selectedChunksKey.debugId
    }`;
    const valid = alreadyValidatedParents.get(parentValidationKey);
    if (valid === false) return;
    if (valid === undefined) {
      // Module can only be moved into the existing chunk if the existing chunk
      // is a parent of all selected chunks
      let isInAllParents = true;
      /** @type {Set<ChunkGroup>} */
      const queue = new Set();
      for (const chunk of selectedChunks) {
        for (const group of chunk.groupsIterable) {
          queue.add(group);
        }
      }
      for (const group of queue) {
        if (existingChunk.isInGroup(group)) continue;
        let hasParent = false;
        for (const parent of group.parentsIterable) {
          hasParent = true;
          queue.add(parent);
        }
        if (!hasParent) {
          isInAllParents = false;
        }
      }
      const valid = isInAllParents;
      alreadyValidatedParents.set(parentValidationKey, valid);
      if (!valid) {
        if (!alreadyReportedErrors.has(name)) {
          alreadyReportedErrors.add(name);
          compilation.errors.push(
            new WebpackError(
              "SplitChunksPlugin\n" +
                `Cache group "${cacheGroup.key}" conflicts with existing chunk.\n` +
                `Both have the same name "${name}" and existing chunk is not a parent of the selected modules.\n` +
                "Use a different name for the cache group or make sure that the existing chunk is a parent (e. g. via dependOn).\n" +
                'HINT: You can omit "name" to automatically create a name.\n' +
                "BREAKING CHANGE: webpack < 5 used to allow to use an entrypoint as splitChunk. " +
                "This is no longer allowed when the entrypoint is not a parent of the selected modules.\n" +
                "Remove this entrypoint and add modules to cache group's 'test' instead. " +
                "If you need modules to be evaluated on startup, add them to the existing entrypoints (make them arrays). " +
                "See migration guide of more info."
            )
          );
        }
        return;
      }
    }
  }
  // Create key for maps
  // When it has a name we use the name as key
  // Otherwise we create the key from chunks and cache group key
  // This automatically merges equal names
  const key =
    cacheGroup.key +
    (name ? ` name:${name}` : ` chunks:${keyToString(selectedChunksKey)}`);
  // Add module to maps
  let info = /** @type {ChunksInfoItem} */ (chunksInfoMap.get(key));
  if (info === undefined) {
    chunksInfoMap.set(
      key,
      (info = {
        modules: new SortableSet(undefined, compareModulesByIdentifier),
        cacheGroup,
        cacheGroupIndex,
        name,
        sizes: {},
        chunks: new Set(),
        reusableChunks: new Set(),
        chunksKeys: new Set(),
      })
    );
  }
  const oldSize = info.modules.size;
  info.modules.add(module);
  if (info.modules.size !== oldSize) {
    for (const type of module.getSourceTypes()) {
      info.sizes[type] = (info.sizes[type] || 0) + module.size(type);
    }
  }
  const oldChunksKeysSize = info.chunksKeys.size;
  info.chunksKeys.add(selectedChunksKey);
  if (oldChunksKeysSize !== info.chunksKeys.size) {
    for (const chunk of selectedChunks) {
      info.chunks.add(chunk);
    }
  }
};

这个函数是 SplitChunksPlugin 的核心部分,负责将模块添加到代码分割的信息映射表中。它实现了"哪些模块应该被提取到哪些共享 chunk 中"的决策逻辑。下面是详细解析:

const addModuleToChunksInfoMap = (
  cacheGroup, // 当前使用的缓存组配置
  cacheGroupIndex, // 缓存组的索引(用于排序)
  selectedChunks, // 为当前模块选择的 chunks
  selectedChunksKey, // 这些 chunks 的唯一标识键
  module // 当前处理的模块
) => {
  // ...
};

首先第一步是 minChunks 验证:

// 如果选中的 chunks 数量少于 minChunks 配置,直接返回
if (selectedChunks.length < /** @type {number} */ (cacheGroup.minChunks))
  return;

这确保了只有当模块被足够多的 chunks 引用时,才会被考虑提取到共享 chunk。

第二步确定分割 chunk 的名称:

const name = /** @type {string} */ (
  /** @type {GetName} */ (cacheGroup.getName)(
    module,
    selectedChunks,
    cacheGroup.key
  )
);

根据配置的命名函数和当前上下文生成新 chunk 的名称。

第三步名称冲突检查

const existingChunk = compilation.namedChunks.get(name);
if (existingChunk) {
  // ... 一系列验证逻辑
}

如果生成的名称已经存在,需要验证现有 chunk 是否可以安全地重用:

  • 创建一个唯一的验证键 parentValidationKey

  • 检查是否已经验证过这个键

  • 验证现有 chunk 是否是所有选中 chunks 的父级

  • 如果验证失败,报错并返回

第四步创建唯一键并准备添加到映射:

const key =
  cacheGroup.key +
  (name ? ` name:${name}` : ` chunks:${keyToString(selectedChunksKey)}`);

为 chunksInfoMap 创建一个唯一键,根据是否有名称采用不同的生成策略。

第五步获取或创建 ChunksInfoItem:

let info = /** @type {ChunksInfoItem} */ (chunksInfoMap.get(key));
if (info === undefined) {
  chunksInfoMap.set(
    key,
    (info = {
      /* 初始化新的 ChunksInfoItem */
    })
  );
}

如果这个键对应的信息不存在,创建一个新的 ChunksInfoItem 对象,包含模块集合、大小信息、chunk 集合等。

第六步添加模块并更新大小信息:

const oldSize = info.modules.size;
info.modules.add(module);
if (info.modules.size !== oldSize) {
  for (const type of module.getSourceTypes()) {
    info.sizes[type] = (info.sizes[type] || 0) + module.size(type);
  }
}

将当前模块添加到模块集合,并更新各类型资源的大小信息。

第七步更新 chunks 信息:

const oldChunksKeysSize = info.chunksKeys.size;
info.chunksKeys.add(selectedChunksKey);
if (oldChunksKeysSize !== info.chunksKeys.size) {
  for (const chunk of selectedChunks) {
    info.chunks.add(chunk);
  }
}

添加 chunks 的唯一标识键,并在必要时更新 chunks 集合。

这个函数是 SplitChunksPlugin 的核心,它:

  1. 根据缓存组配置,确定哪些模块应该被提取到共享 chunk

  2. 维护 chunksInfoMap,这是后续创建实际分割 chunks 的基础数据结构

  3. 处理模块、chunks 和大小信息的关联关系

  4. 解决命名冲突和验证现有 chunk 是否可重用

  5. 为最终的代码分割决策准备必要的信息

通过这个函数的执行,webpack 能够准确地收集所有满足分割条件的模块组合信息,从而在后续流程中做出最优的代码分割决策。

模块分组阶段

在完成准备工作后,接下来是核心的分组优化阶段。插件会遍历所有的模块,将符合条件的模块通过 addModuleToChunksInfoMap 方法存储到 chunksInfoMap 中,以便为后续的代码分割决策提供基础数据。

for (const module of compilation.modules) {
  // Get cache group
  const cacheGroups = this.options.getCacheGroups(module, context);
  if (!Array.isArray(cacheGroups) || cacheGroups.length === 0) {
    continue;
  }

  // Prepare some values (usedExports = false)
  const getCombs = memoize(() => {
    const chunks = chunkGraph.getModuleChunksIterable(module);
    const chunksKey = getKey(chunks);
    return getCombinations(chunksKey);
  });

  // Prepare some values (usedExports = true)
  const getCombsByUsedExports = memoize(() => {
    // fill the groupedByExportsMap
    getExportsChunkSetsInGraph();
    /** @type {Set<Set<Chunk> | Chunk>} */
    const set = new Set();
    const groupedByUsedExports =
      /** @type {Iterable<Chunk[]>} */
      (groupedByExportsMap.get(module));
    for (const chunks of groupedByUsedExports) {
      const chunksKey = getKey(chunks);
      for (const comb of getExportsCombinations(chunksKey)) set.add(comb);
    }
    return set;
  });

  let cacheGroupIndex = 0;
  for (const cacheGroupSource of cacheGroups) {
    const cacheGroup = this._getCacheGroup(cacheGroupSource);

    const combs = cacheGroup.usedExports ? getCombsByUsedExports() : getCombs();
    // For all combination of chunk selection
    for (const chunkCombination of combs) {
      // Break if minimum number of chunks is not reached
      const count =
        chunkCombination instanceof Chunk ? 1 : chunkCombination.size;
      if (count < /** @type {number} */ (cacheGroup.minChunks)) continue;
      // Select chunks by configuration
      const { chunks: selectedChunks, key: selectedChunksKey } =
        getSelectedChunks(
          chunkCombination,
          /** @type {ChunkFilterFunction} */
          (cacheGroup.chunksFilter)
        );

      addModuleToChunksInfoMap(
        cacheGroup,
        cacheGroupIndex,
        selectedChunks,
        selectedChunksKey,
        module
      );
    }
    cacheGroupIndex++;
  }
}

这段代码是 SplitChunksPlugin 的核心分组阶段,它遍历所有模块并确定它们应该如何被分配到不同的共享 chunks 中。下面是详细流程:

首先是获取模块的缓存组:

const cacheGroups = this.options.getCacheGroups(module, context);
if (!Array.isArray(cacheGroups) || cacheGroups.length === 0) {
    continue;
}

根据配置确定该模块适用哪些缓存组。如果没有匹配的缓存组,则跳过该模块。

这里有两种准备组合的方式,根据是否启用 usedExports 选项:

// 标准方式
const getCombs = memoize(() => {
  const chunks = chunkGraph.getModuleChunksIterable(module);
  const chunksKey = getKey(chunks);
  return getCombinations(chunksKey);
});

// 基于导出使用情况的方式
const getCombsByUsedExports = memoize(() => {
  // ... 获取按导出分组的 chunks 组合
});

这两个函数都通过 memoize 实现缓存,避免重复计算。它们的作用是找出所有包含当前模块的 chunks 组合。

接着遍历缓存组并处理每种组合

let cacheGroupIndex = 0;
for (const cacheGroupSource of cacheGroups) {
  const cacheGroup = this._getCacheGroup(cacheGroupSource);

  const combs = cacheGroup.usedExports ? getCombsByUsedExports() : getCombs();
  // ...
}

对每个适用的缓存组,SplitChunksPlugin 先获取其规范化配置,然后根据是否启用 usedExports 选择相应的 模块-chunks 组合获取方式,以确定哪些模块组合满足分割条件。

遍历所有可能的 chunk 组合

for (const chunkCombination of combs) {
  // 检查 minChunks 条件
  const count = chunkCombination instanceof Chunk ? 1 : chunkCombination.size;
  if (count < cacheGroup.minChunks) continue;

  // 根据配置过滤 chunks
  const { chunks: selectedChunks, key: selectedChunksKey } = getSelectedChunks(
    chunkCombination,
    cacheGroup.chunksFilter
  );

  // 添加到 chunksInfoMap
  addModuleToChunksInfoMap(
    cacheGroup,
    cacheGroupIndex,
    selectedChunks,
    selectedChunksKey,
    module
  );
}

对每个组合:

  1. 检查是否满足 minChunks 条件(模块至少要在多少个 chunks 中出现)

  2. 根据缓存组的 chunksFilter(如 "initial"、"async" 或 "all")过滤 chunks

  3. 将符合条件的模块和 chunks 信息添加到 chunksInfoMap

这个分组阶段的核心目的是:

  1. 收集信息:确定哪些模块被哪些 chunks 引用,以及它们如何组合

  2. 应用规则:根据 SplitChunks 配置(如 minChunks, chunksFilter)过滤组合

  3. 构建数据结构:将所有符合条件的模块-chunks 组合添加到 chunksInfoMap

chunksInfoMap 将在后续阶段被用于实际创建共享 chunks。这个阶段不进行实际的 chunk 创建,而是准备决策所需的所有信息。

通过这个复杂但高效的流程,webpack 能够智能地决定哪些模块应该被提取到共享 chunks 中,从而达到代码分割的优化目标。

20250413104511

在 SplitChunksPlugin 的逻辑里,有个很重要的判断,就是看一个模块是不是被多个 chunk 引用了,也就是 "chunks 数量 ≥ minChunks?" 这个条件。

简单来说,minChunks 的意思就是:这个模块得被多少个地方用到,Webpack 才觉得它“值”得被单独拆出去。比如你设置了 minChunks: 2,那一个模块至少被两个 chunk 用了,才会被考虑拆出来当公共模块。

为啥要这么干?因为如果一个模块只在一个地方用,没必要拆啊,拆出来还多一次网络请求,反而拖慢加载速度。所以这个判断就是为了避免乱拆,只提取真正“多人共用”的模块,这样打包出来才更高效。

也可以理解为,用这个条件来控制拆分的“尺度”,想多拆就设低点,想少拆就设高点~

依次检查阶段

在模块分组阶段,我们根据缓存组配置将模块按共享关系初步归类,并存入 chunksInfoMap。而在分组优化阶段,webpack 会进一步筛选 chunksInfoMap 中的每个条目,确保它们严格符合用户设定的 minSize、minChunks 等约束条件,对不满足条件的分组进行剔除,最终只保留真正有价值且符合所有优化规则的代码分割方案。

// Filter items were size < minSize
for (const [key, info] of chunksInfoMap) {
  if (removeMinSizeViolatingModules(info)) {
    chunksInfoMap.delete(key);
  } else if (
    !checkMinSizeReduction(
      info.sizes,
      info.cacheGroup.minSizeReduction,
      info.chunks.size
    )
  ) {
    chunksInfoMap.delete(key);
  }
}

/**
 * @typedef {object} MaxSizeQueueItem
 * @property {SplitChunksSizes} minSize
 * @property {SplitChunksSizes} maxAsyncSize
 * @property {SplitChunksSizes} maxInitialSize
 * @property {string} automaticNameDelimiter
 * @property {string[]} keys
 */

/** @type {Map<Chunk, MaxSizeQueueItem>} */
const maxSizeQueueMap = new Map();

while (chunksInfoMap.size > 0) {
  // Find best matching entry
  let bestEntryKey;
  let bestEntry;
  for (const pair of chunksInfoMap) {
    const key = pair[0];
    const info = pair[1];
    if (bestEntry === undefined || compareEntries(bestEntry, info) < 0) {
      bestEntry = info;
      bestEntryKey = key;
    }
  }

  const item = /** @type {ChunksInfoItem} */ (bestEntry);
  chunksInfoMap.delete(/** @type {string} */ (bestEntryKey));

  /** @type {Chunk["name"] | undefined} */
  let chunkName = item.name;
  // Variable for the new chunk (lazy created)
  /** @type {Chunk | undefined} */
  let newChunk;
  // When no chunk name, check if we can reuse a chunk instead of creating a new one
  let isExistingChunk = false;
  let isReusedWithAllModules = false;
  if (chunkName) {
    const chunkByName = compilation.namedChunks.get(chunkName);
    if (chunkByName !== undefined) {
      newChunk = chunkByName;
      const oldSize = item.chunks.size;
      item.chunks.delete(newChunk);
      isExistingChunk = item.chunks.size !== oldSize;
    }
  } else if (item.cacheGroup.reuseExistingChunk) {
    outer: for (const chunk of item.chunks) {
      if (chunkGraph.getNumberOfChunkModules(chunk) !== item.modules.size) {
        continue;
      }
      if (
        item.chunks.size > 1 &&
        chunkGraph.getNumberOfEntryModules(chunk) > 0
      ) {
        continue;
      }
      for (const module of item.modules) {
        if (!chunkGraph.isModuleInChunk(module, chunk)) {
          continue outer;
        }
      }
      if (!newChunk || !newChunk.name) {
        newChunk = chunk;
      } else if (chunk.name && chunk.name.length < newChunk.name.length) {
        newChunk = chunk;
      } else if (
        chunk.name &&
        chunk.name.length === newChunk.name.length &&
        chunk.name < newChunk.name
      ) {
        newChunk = chunk;
      }
    }
    if (newChunk) {
      item.chunks.delete(newChunk);
      chunkName = undefined;
      isExistingChunk = true;
      isReusedWithAllModules = true;
    }
  }

  const enforced =
    item.cacheGroup._conditionalEnforce &&
    checkMinSize(item.sizes, item.cacheGroup.enforceSizeThreshold);

  const usedChunks = new Set(item.chunks);

  // Check if maxRequests condition can be fulfilled
  if (
    !enforced &&
    (Number.isFinite(item.cacheGroup.maxInitialRequests) ||
      Number.isFinite(item.cacheGroup.maxAsyncRequests))
  ) {
    for (const chunk of usedChunks) {
      // respect max requests
      const maxRequests = /** @type {number} */ (
        chunk.isOnlyInitial()
          ? item.cacheGroup.maxInitialRequests
          : chunk.canBeInitial()
          ? Math.min(
              /** @type {number} */
              (item.cacheGroup.maxInitialRequests),
              /** @type {number} */
              (item.cacheGroup.maxAsyncRequests)
            )
          : item.cacheGroup.maxAsyncRequests
      );
      if (Number.isFinite(maxRequests) && getRequests(chunk) >= maxRequests) {
        usedChunks.delete(chunk);
      }
    }
  }

  outer: for (const chunk of usedChunks) {
    for (const module of item.modules) {
      if (chunkGraph.isModuleInChunk(module, chunk)) continue outer;
    }
    usedChunks.delete(chunk);
  }

  // Were some (invalid) chunks removed from usedChunks?
  // => readd all modules to the queue, as things could have been changed
  if (usedChunks.size < item.chunks.size) {
    if (isExistingChunk) usedChunks.add(/** @type {Chunk} */ (newChunk));
    if (
      /** @type {number} */ (usedChunks.size) >=
      /** @type {number} */ (item.cacheGroup.minChunks)
    ) {
      const chunksArr = Array.from(usedChunks);
      for (const module of item.modules) {
        addModuleToChunksInfoMap(
          item.cacheGroup,
          item.cacheGroupIndex,
          chunksArr,
          getKey(usedChunks),
          module
        );
      }
    }
    continue;
  }

  // Validate minRemainingSize constraint when a single chunk is left over
  if (
    !enforced &&
    item.cacheGroup._validateRemainingSize &&
    usedChunks.size === 1
  ) {
    const [chunk] = usedChunks;
    const chunkSizes = Object.create(null);
    for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
      if (!item.modules.has(module)) {
        for (const type of module.getSourceTypes()) {
          chunkSizes[type] = (chunkSizes[type] || 0) + module.size(type);
        }
      }
    }
    const violatingSizes = getViolatingMinSizes(
      chunkSizes,
      item.cacheGroup.minRemainingSize
    );
    if (violatingSizes !== undefined) {
      const oldModulesSize = item.modules.size;
      removeModulesWithSourceType(item, violatingSizes);
      if (item.modules.size > 0 && item.modules.size !== oldModulesSize) {
        // queue this item again to be processed again
        // without violating modules
        chunksInfoMap.set(/** @type {string} */ (bestEntryKey), item);
      }
      continue;
    }
  }

  // Create the new chunk if not reusing one
  if (newChunk === undefined) {
    newChunk = compilation.addChunk(chunkName);
  }
  // Walk through all chunks
  for (const chunk of usedChunks) {
    // Add graph connections for splitted chunk
    chunk.split(newChunk);
  }

  // Add a note to the chunk
  newChunk.chunkReason =
    (newChunk.chunkReason ? `${newChunk.chunkReason}, ` : "") +
    (isReusedWithAllModules ? "reused as split chunk" : "split chunk");
  if (item.cacheGroup.key) {
    newChunk.chunkReason += ` (cache group: ${item.cacheGroup.key})`;
  }
  if (chunkName) {
    newChunk.chunkReason += ` (name: ${chunkName})`;
  }
  if (item.cacheGroup.filename) {
    newChunk.filenameTemplate = item.cacheGroup.filename;
  }
  if (item.cacheGroup.idHint) {
    newChunk.idNameHints.add(item.cacheGroup.idHint);
  }
  if (!isReusedWithAllModules) {
    // Add all modules to the new chunk
    for (const module of item.modules) {
      if (!module.chunkCondition(newChunk, compilation)) continue;
      // Add module to new chunk
      chunkGraph.connectChunkAndModule(newChunk, module);
      // Remove module from used chunks
      for (const chunk of usedChunks) {
        chunkGraph.disconnectChunkAndModule(chunk, module);
      }
    }
  } else {
    // Remove all modules from used chunks
    for (const module of item.modules) {
      for (const chunk of usedChunks) {
        chunkGraph.disconnectChunkAndModule(chunk, module);
      }
    }
  }

  if (
    Object.keys(item.cacheGroup.maxAsyncSize).length > 0 ||
    Object.keys(item.cacheGroup.maxInitialSize).length > 0
  ) {
    const oldMaxSizeSettings = maxSizeQueueMap.get(newChunk);
    maxSizeQueueMap.set(newChunk, {
      minSize: oldMaxSizeSettings
        ? combineSizes(
            oldMaxSizeSettings.minSize,
            item.cacheGroup._minSizeForMaxSize,
            Math.max
          )
        : item.cacheGroup.minSize,
      maxAsyncSize: oldMaxSizeSettings
        ? combineSizes(
            oldMaxSizeSettings.maxAsyncSize,
            item.cacheGroup.maxAsyncSize,
            Math.min
          )
        : item.cacheGroup.maxAsyncSize,
      maxInitialSize: oldMaxSizeSettings
        ? combineSizes(
            oldMaxSizeSettings.maxInitialSize,
            item.cacheGroup.maxInitialSize,
            Math.min
          )
        : item.cacheGroup.maxInitialSize,
      automaticNameDelimiter: item.cacheGroup.automaticNameDelimiter,
      keys: oldMaxSizeSettings
        ? oldMaxSizeSettings.keys.concat(item.cacheGroup.key)
        : [item.cacheGroup.key],
    });
  }

  // remove all modules from other entries and update size
  for (const [key, info] of chunksInfoMap) {
    if (isOverlap(info.chunks, usedChunks)) {
      // update modules and total size
      // may remove it from the map when < minSize
      let updated = false;
      for (const module of item.modules) {
        if (info.modules.has(module)) {
          // remove module
          info.modules.delete(module);
          // update size
          for (const key of module.getSourceTypes()) {
            info.sizes[key] -= module.size(key);
          }
          updated = true;
        }
      }
      if (updated) {
        if (info.modules.size === 0) {
          chunksInfoMap.delete(key);
          continue;
        }
        if (
          removeMinSizeViolatingModules(info) ||
          !checkMinSizeReduction(
            info.sizes,
            info.cacheGroup.minSizeReduction,
            info.chunks.size
          )
        ) {
          chunksInfoMap.delete(key);
          continue;
        }
      }
    }
  }
}

这段代码是 SplitChunksPlugin 中至关重要的分组优化和实际执行阶段,主要包含筛选、排序、创建和连接新 chunks 的完整流程。我将详细解析其执行过程:

1. 初始筛选阶段

// 筛选不满足大小要求的项
for (const [key, info] of chunksInfoMap) {
  if (removeMinSizeViolatingModules(info)) {
    chunksInfoMap.delete(key);
  } else if (
    !checkMinSizeReduction(
      info.sizes,
      info.cacheGroup.minSizeReduction,
      info.chunks.size
    )
  ) {
    chunksInfoMap.delete(key);
  }
}

这一步完成两项重要筛选:

  • 通过 removeMinSizeViolatingModules 移除不满足 minSize 的模块

  • 检查分割后的大小减少是否满足 minSizeReduction 的要求

  • 不满足条件的分组会被直接从 chunksInfoMap 中删除

2. 迭代处理分组

接下来,代码进入一个 while 循环,直到处理完 chunksInfoMap 中的所有条目:

while (chunksInfoMap.size > 0) {
  // 处理逻辑...
}

3. 选择最佳分组

// 找到最佳匹配项
let bestEntryKey;
let bestEntry;
for (const pair of chunksInfoMap) {
  const key = pair[0];
  const info = pair[1];
  if (bestEntry === undefined || compareEntries(bestEntry, info) < 0) {
    bestEntry = info;
    bestEntryKey = key;
  }
}

const item = bestEntry;
chunksInfoMap.delete(bestEntryKey);

这一步使用 compareEntries 函数比较所有分组,找出最优先处理的项。比较标准包括:

  1. 缓存组优先级 (priority)

  2. chunks 数量

  3. 可减少的大小

  4. 缓存组索引

  5. 模块数量和标识符

4. 检查 chunk 复用可能性

let chunkName = item.name;
let newChunk;
let isExistingChunk = false;
let isReusedWithAllModules = false;

// 尝试复用已命名的 chunk
if (chunkName) {
  const chunkByName = compilation.namedChunks.get(chunkName);
  if (chunkByName !== undefined) {
    // 复用逻辑...
  }
}
// 尝试复用现有 chunk
else if (item.cacheGroup.reuseExistingChunk) {
  // 复杂的复用查找逻辑...
}

这部分实现了两种 chunk 复用策略:

  • 如果有名称,尝试复用同名 chunk

  • 如果配置了 reuseExistingChunk,查找完全匹配模块集的现有 chunk

5. 应用限制条件

const enforced = item.cacheGroup._conditionalEnforce &&
                 checkMinSize(item.sizes, item.cacheGroup.enforceSizeThreshold);

const usedChunks = new Set(item.chunks);

// 检查 maxRequests 条件
if (!enforced && (Number.isFinite(...) || Number.isFinite(...))) {
    // 移除超过请求数限制的 chunks
}

// 移除不包含任何分割模块的 chunks
outer: for (const chunk of usedChunks) {
    // 检查逻辑...
}

这里应用了几项重要限制:

  • 检查 enforceSizeThreshold 以决定是否强制执行

  • 应用 maxInitialRequestsmaxAsyncRequests 限制

  • 移除那些实际上不需要分割的 chunks

6. 处理 chunks 变更与重新验证

// 如果移除了一些 chunks,重新处理这些模块
if (usedChunks.size < item.chunks.size) {
    // 重新添加到队列...
    continue;
}

// 验证 minRemainingSize 约束
if (!enforced && item.cacheGroup._validateRemainingSize && usedChunks.size === 1) {
    // 验证剩余大小...
}

这部分处理两种特殊情况:

  • 当可用 chunks 减少时,重新将模块添加到处理队列

  • 当只剩一个 chunk 时,验证剩余模块大小是否满足 minRemainingSize

7. 创建或连接新 chunk

// 如果不复用,创建新 chunk
if (newChunk === undefined) {
    newChunk = compilation.addChunk(chunkName);
}

// 为所有使用的 chunks 建立分割关系
for (const chunk of usedChunks) {
    chunk.split(newChunk);
}

// 设置 chunk 的各种属性
newChunk.chunkReason = ...;
if (item.cacheGroup.filename) {
    newChunk.filenameTemplate = item.cacheGroup.filename;
}

这一步完成 chunk 的实际创建或连接:

  • 如果需要,创建新的 chunk

  • 建立原始 chunks 与新 chunk 的分割关系

  • 设置各种元数据(原因、文件名模板等)

8. 移动模块到新 chunk

if (!isReusedWithAllModules) {
  // 将所有模块添加到新 chunk
  for (const module of item.modules) {
    if (!module.chunkCondition(newChunk, compilation)) continue;
    // 添加模块到新 chunk
    chunkGraph.connectChunkAndModule(newChunk, module);
    // 从原 chunks 中移除模块
    for (const chunk of usedChunks) {
      chunkGraph.disconnectChunkAndModule(chunk, module);
    }
  }
} else {
  // 从原 chunks 中移除所有模块
  for (const module of item.modules) {
    for (const chunk of usedChunks) {
      chunkGraph.disconnectChunkAndModule(chunk, module);
    }
  }
}

根据复用情况,有两种处理方式:

  • 常规情况:将模块连接到新 chunk,并从原 chunks 断开连接
  • 完全复用情况:只需从原 chunks 断开连接

9. 设置 maxSize 参数

if (Object.keys(...).length > 0 || Object.keys(...).length > 0) {
    // 设置 maxSize 相关参数...
}

为支持后续的 maxSize 处理,设置相关参数。

10. 更新其他分组

// 从其他分组中移除已处理的模块并更新大小
for (const [key, info] of chunksInfoMap) {
  if (isOverlap(info.chunks, usedChunks)) {
    // 更新模块和总大小...
  }
}

最后,这一步处理连锁效应:

  • 检查其他分组是否与当前处理的 chunks 有重叠

  • 移除这些分组中已处理的模块

  • 更新大小信息,可能会导致其他分组因不满足条件而被删除

执行流程总结

20250413111822

SplitChunksPlugin 的执行流程可以概括为以下关键步骤:

  1. 初始筛选:首先清理 chunksInfoMap,移除不满足 minSize 或 minSizeReduction 条件的分组。

  2. 优先级处理:循环处理 chunksInfoMap 中的条目,每次选取优先级最高的分组(基于 priority、chunks 数量和大小)。

  3. 尝试复用:检查是否可以复用现有 chunk,优先使用同名 chunk 或符合条件的已有 chunk。

  4. 应用限制:过滤不符合 maxInitialRequests 和 maxAsyncRequests 限制的 chunks。

  5. 创建分割:创建新 chunk(或使用复用的 chunk),建立与原 chunks 的分割关系。

  6. 移动模块:将模块从原 chunks 移动到新 chunk,更新相应的连接关系。

  7. 更新影响:处理对其他分组的连锁影响,移除已处理的模块,可能导致其他分组不再满足条件被删除。

  8. 循环继续:重复以上过程直到 chunksInfoMap 为空,完成所有可能的代码分割。

本质上,Webpack 在这一阶段的工作就是:找出最值得拆分的模块组合 → 检查各种限制条件 → 创建或复用 chunk → 转移模块 → 更新受影响的其他分组,不断循环直到处理完所有符合条件的组合。

整个流程确保了按照用户配置的优先级和条件智能地创建共享 chunks,优化最终的 bundle 结构和大小。

总结

SplitChunks 的作用就是帮我们把多个模块打包成更合理的共享 chunk,避免重复打包、提高缓存效率。Webpack 会先根据配置(比如 minSize、minChunks 等)过滤掉不需要拆的模块,然后把“被多个 chunk 引用”的模块挑出来,认为它们值得被单独提取。接着,它会通过一系列判断(比如 chunk 是否能复用、是否超过请求数量限制)来决定是创建新 chunk,还是复用旧的。每当一个 chunk 被生成,它还会更新其他相关分组,避免重复拆包。整个过程其实就是“挑模块 ➜ 检查规则 ➜ 创建 chunk ➜ 移模块 ➜ 循环处理”,直到没有模块再需要拆为止。

总之,它的目标就是:提取公共代码,优化加载性能,让你的项目打包结果又小又高效~ 💥

万字webpack精华总结:那些面试官提问的实现原理你答上来了吗

作者 原生高钙
2025年4月12日 16:15

webpack概述:

webpack是一款常见的前端构建工具。它通过分析项目中的模块依赖关系,将各类资源视为模块。对于这些资源,一部分是像 Html,Css,js等浏览器认识的资源,另一部分是 jsx,ts,vue等浏览器不认识的资源,这时候就需要用到 webpack 这种构建工具去打包,去将这些不认识的代码转为浏览器认识的代码。webpack使用的是js代码进行开发,基于Node平台去运行。

通过本篇文章能学到什么:

  • webpack是怎么工作的

  • webpack用到什么工具去做这些构建打包工作的

  • 常见优化性能的实现原理详解

webpack构建流程:

webpack的构建过程分为三部分:初始化,编译,输出。

初始化:

  1. 读取配置:通过配置文件(webpack.config.js)或合并命令行参数(--mode)读取配置

  2. 创建Compiler对象:负责构建整个生命周期(启动,监听,关闭)

  3. 加载插件:调用每个插件的apply方法,将插件挂载到Compiler的生命周期上的钩子上。

  4. 初始化默认配置:根据mode(development/production)设置默认优化规则

编译:

  1. 从入口文件开始构建依赖图:根据entry找到入口文件

  2. 解析模块:

    1. 调用Loader:对模块内容进行翻译,转换
    2. 生成AST:将代码转为抽象语法树,分析模块的依赖关系(如import,require)
    3. 递归处理依赖
  3. 生成模块记录:保存每个模块的信息(如源码,依赖路径,转换后的代码)

输出:

  1. 生成chunk:根据依赖图和配置的代码分割(splitChunks),将模块组合成 Chunk
  2. 优化chunk:执行插件定义的优化操作(如 TerserPlugin 压缩代码,SplitChunksPlugin 拆分公共代码)
  3. 生成最终文件:

  将 Chunk 转为浏览器可执行的代码

  根据output配置,将文件写入磁盘

总结:

  • 初始化:启动构建,读取并合并配置参数,加载plugin,实例化compiler

  • 编译:从入口文件触发,找到每个module串行调用对应的loader,再找到该模块依赖的模块,递归地进行编译处理

  • 输出:将编译后地module组合成chunk,将chunk转换为文件,输出到文件系统

webpack打包流程:

首先,到这大家肯定有一个疑问,构建工程和打包过程不是一个东西吗???

严格来说,构建过程强调的是从源码到产物的完整处理过程,而打包过程更侧重于最终生成文件的结果阶段

构建过程和打包过程的区别:

流程 定义 阶段覆盖范围
构建过程 从读取配置到生成最终产物的完整过程,包括初始化、编译、优化等所有步骤。 包含从启动到输出的全部阶段(广义)。
打包流程 通常指将处理后的模块(Module)组合成 Chunk,并输出为文件(Bundle)的具体过程。 更侧重于编译后的输出阶段(狭义)。

打包流程:

  1. 从 Module 到 Chunk

    1. 根据入口文件代码分割规则(如 import() 动态导入或 splitChunks 配置),将关联的模块分组为 Chunk

    2. 每个 Chunk 包含:

      • 入口模块及其依赖的所有模块。
      • 运行时代码(Webpack 自执行的胶水代码,用于模块加载和管理)。
  2. 优化 Chunk

    1. Tree Shaking:删除未被使用的代码(仅 production 模式生效)。
    2. 代码压缩:通过 TerserPlugin 压缩 JS,CssMinimizerPlugin 压缩 CSS。
    3. 作用域 提升(Scope Hoisting) :合并模块作用域,减少闭包代码体积。
    4. 公共代码提取:通过 SplitChunksPlugin 拆分重复依赖(如 node_modules 中的库)。
  3. 生成 Bundle 文件

  1. 将 Chunk 转换为最终的可执行文件(Bundle),包括:

    • JS Bundle:包含模块代码和 Webpack 运行时逻辑。
    • CSS Bundle:若使用 MiniCssExtractPlugin,CSS 会被提取为独立文件。
    • 资源文件:如图片、字体通过 file-loader 输出到指定目录。
  1. 写入文件系统

    1. 根据 output 配置,将 Bundle 和资源文件写入磁盘(如 dist/ 目录)。
    2. 生成辅助文件(如 manifest.jsonindex.html 或 SourceMap)。

webpack Loader

webpack默认值只认识 js 文件,需要 Loader 去充当“翻译官”的角色,去识别其他不认识的文件

Css-Loader

在我们模块化开发时,需要将 css 文件和 js 文件分隔开

我们若想让浏览器识别 css 文件,则需要两个步骤:

  1. 首先使用 css-loader 进行依赖图中 css 文件的识别,进行 css 文件的解析
  2. 再使用 style-loader 将 css 链接到 index.html 文件中,让 css 发挥出真正的效果
module.exports={
        module:{
                rules:[
                        {
                                test:/.css$/, //解释见[1]
                use:[ //记录需使用的loader,注意这里是从后往前使用,即先使用css-loader再使用style—loader
                    {loader:style-loader}, //将css文件链接至index.html
                    {loader:css-loader} //解析css文件
                ]
                        }
                ]
        }
}
//[1]这里使用的是正则表达式,/ /代表正则表达式的开始与结束,.代表.(.在正则中有特定含义,需要用\转义),$表示需要以.css结尾。

Less-Loader

less-loader是用来解析 less 文件,分为三步:

  1. 首先使用 less-loader 将 less 文件转为 css 文件(需要借助 less 库进行转化)
  2. 再使用 css-loader 进行依赖图中 css 文件的识别,进行 css 文件的解析
  3. 最后使用 style-loader 将 css 链接到 index.html 文件中,让 css 发挥出真正的效果
module.exports={
    module:{
        rules:[
            {
                test:"/.less$/",
                use:[
                    {loader:"style-loader"},
                            {laoder:"css-loader"},
                            {loader:"less-loader"}
                ]
            }
                
            ]
    }

}

Postcss-Loader

postcss是一个样式转换工具,对 css 进行转化和适配,比如加一些浏览器前缀以适配不同浏览器,将 px 转为 rem 或 vh。但完成上述功能需要 postcss 对应不同插件的支持:

  • Autoprefixer 和 PostCSS-preset-env 的区别
特性 Autoprefixer PostCSS-preset-env
功能范围 仅添加前缀 前缀+未来 Css 语法转换
Css 新特性支持 不支持 支持
配置复杂度 简单 较复杂
独立性 可单独使用 内置Autoprefixer
  • 二者适用场景

如果只需处理浏览器前缀,用 Autoprefixer(更轻量)。

如果需要使用 CSS 新特性(如嵌套、逻辑属性等),用 PostCSS-preset-env

   两者通常一起使用:PostCSS-preset-env 负责语法转换,Autoprefixer 处理前缀(即使 preset-env 内置了 Autoprefixer,也可单独配置)。

Babel-Loader

Babel 是负责在低版本的浏览器环境中依然能运行 es6+ 这种新特性语法,但每个新特性都需要对应插件的支持。

module.exports={
    test:/.js$/,
    use:[
            {
                loader:"babel-loader",
                options:{
                    plugins:[ //babel使用的插件
                        "@babel/plugin-transform-arrow-functions",
                        "@babel/plugin-transform-block-scoping"
                    ]
                }
            }
    ],

}

然而这些插件又多又难记,因此实际开发中一般给 webpack 提供一个预设preset,webpack 根据预设来加载对应插件列表并传递给Babel:

module.exports={
    test:/.js$/,
    use:[
            {
                loader:"babel-loader",
                options:{
                          presets:["@babel/preset-env"] //注意这里是预设preset配置项,已不再是plugins
                }
            }
    ],

}

webpack plugin

Plugin 概述:

loader代表加载器,只能完成不同模块类型的解析,plugin代表插件,可以做更加广泛的任务,贯穿于Webpack整个生命周期。

Plugin 的作用:

  1. 打包优化,将样式单独抽取为一个文件并在index.html中用link方式引用,而不是用style标签直接嵌入到html页面中。

  2. 资源管理,二次打包前先删除上次打包后的文件夹,可以使得二次打包不需要的文件(如图片)自动删除。

  3. 环境变量注入,使得变量一处定义,处处可用。

常见 plugin:

资源管理 clean 插件: clean-webpack-plugin 可以使得二次打包前先删除打包后的文件夹,即二次打包不需要的文件删除

import {CleanWebpackPlugin} from "clean-webpack-plugin"
module.exports={
    plugin:[new CleanWebpackPlugin()]
}

Html 插件: 默认情况下,打包后只有一个 js 文件,然后需要手动添加一个 html 文件引入 js 文件。html-webpack-plugin 就是帮我们自动生成 html 文件的

import {CleanWebpackPlugin} from "clean-webpack-plugin"
module.exports={
    plugin:[new CleanWebpackPlugin()]
}

DefinePlugin:define-plugin插件用于环境变量注入,使得变量一处定义,处处使用。该插件是 webpack 内置的插件,直接从 webpack 库中引入即可:

const {DefinePlugin} from "webpack"
module.exports={
    plugins:[
        new DefinePlugin({
            "BASE_URL":"./" //配置BASE_URL变量的值为"./"
        })
    ]
}

自定义 plugin:

  1. 目的: 自定义 plugin可以让 webpack 做自己定制的需求,执行特定的任务,有更强的构建能力
  2. 工作原理:

Webpack 就像一条生产线,插件就像插入生产线的一个功能

   Webpack 在编译代码过程中,会触发一系列钩子事件,插件做的就是找到对应钩子,往上面挂上自己的任务,也就是注册事件。

   当 webpack 构建的时候,插件就会找到对应钩子触发执行了

  1. 理解 compiler 和 compilation:

compiler是贯穿了整个 webpack 生命周期的一个实例,代表了完整的 webpack 的环境配置

compilation是每次新的资源构建时创建的一个实例,代表一次单独的版本构建

  1. 怎么实现自定义 plugin:

搭建插件的基本结构:需要 apply 方法,接受 compiler 对象作为参数

在 apply 方法中注册生命周期钩子

生成版本文件 用 node 在 debugger 行设置断点进行调试

//需求:给打包输出文件添加注释

const { compilation } = require("webpack");

//开发思路:
//触发时机:生成资源到输出目录之前 emit钩子
//如何获取打包输出的资源?compilation可以获取所有的即将输出的资源文件

class BannerWebpackPlugin{
    constructor(){}
    apply(compiler){
        compiler.hooks.emit.tap('BannerWebpackPlugin',(compilation)=>{
            debugger;
            const extensions=['css','js'];
            //过滤只保留这两种文件
            const assets=Object.keys(compilation.assets).filter((assetpath)=>{
                const splitted=assetpath.split('.');
                const extension=splitted[splitted.length-1];
                return extensions.includes(extension)
            })
            const prefix = '/* Built by BannerWebpackPlugin */\n';
            assets.forEach((asset)=>{
                const source=compilation.assets[asset].source();//原来内容
                const content=prefix+source; //拼接上注释
                //修改资源
                compilation.assets[asset]={
                    source(){
                        return content; //资源内容
                    },
                    size(){
                        return content.length
                    }
                }
            })
        })
    }
}

module.exports=BannerWebpackPlugin

plugin和loader的区别:

  • webpack只认识JavaScript,Loader就是充当一个翻译官的角色,对其他类型的资源做转译的预处理工作

  • Plugin就是插件,基于事件流框架Tapable,扩展Webpack的功能,贯穿于Webpack的整个生命周期

  • Loader在module.rules中配置,类型为数组,每一项都是Object

  • Plugin在plugins中单独配置,类型为数组,每一项都是一个plugin的实例,参数通过构造函数传入

webpack的常见优化手段整理:

  1. 并行构建优化:

多进程多实例构建:thread-loader

并行压缩:terser-webpack-plugin

  1. 缩小构建范围:

精确指定 loader 的作用范围

优化模块解析

排除无用模块

  1. 缓存利用:

持久化缓存:cache属性

Loader 缓存:babel-loader开启缓存

HardSourceWebpackPlugin插件

  1. 代码拆分与按需加载:
  2. Tree shaking 与 scope hosting
  3. 图片压缩:image-webpack-plugin
  4. 按需加载 polyfill

热更新HMR

  1. HMR 概述: 它能够让你在不刷新页面的前提下,自动更新修改的部分,同时还能保留页面状态
  2. HMR的核心就是客户端向服务器拉取更新后的文件,即 chunk diff
  3. 实际上 webpack-dev-server 与浏览器上维护了一个websocket,当本地资源变化后,wds会立刻向浏览器推送更新,并带上构建时的hash,与上一次的构建做对比。
  4. 客户端对比出差异后,会向WDS发送AJAX请求来获取这些更改内容,后续客户端就可以借助这些信息发送JSONP请求获取 chunk 的增量更新。

树摇 Tree shaking

Tree shaking 基于ESM的静态结构特性,通过分析模块间的导入导出关系,精确地识别并移除未被引用的代码

实现原理:

  1. 在编辑阶段,提取所有导出变量并记录到模块依赖图的结构中,构建完整的模块关系图并标记未使用的导出
  2. 随后,在优化阶段,Terser等压缩工具会识别并物理移除未使用的代码,最终生成精简的产物文件

为什么CommonJS 不支持 Tree shaking?

  1. CommonJS 模块系统允许高度动态的导入模式
  2. Cjs 导出的是完整的模块对象
  3. Cjs 无法在编译时确定使用了哪些具体导出项,依赖解析发生在执行阶段,模块依赖图只有在代码执行时才能完全确定
  4. 静态分析工具无法可靠地预测所有可能的模块加载路径

代码分割

  • 代码分割就是将一个大的bundle拆分成多个小的chunks

  • 代码分割的本质是:将代码拆解为更小的,可独立加载的模块单元,在全量加载和零散请求之间找到性能最优解,将可控的复杂度换取用户体验与工程效率的平衡

  • 要抽离的代码:

    • 公共模块的代码不需要重复打包,单独抽离成一个文件,直接引用即可
    • 第三方模块的代码,单独抽离成一个文件,然后比如lodash
  • 核心拆解:

    • 以一个电商网站的长页面为例,
    • 要首先做首屏加载
    • 再做交互时加载,后续分批按需加载
  • 意义:这样做既不会让浏览器内存超载,又可以逐步扩展功能

Source Map:

  1. Source Map 概述: Source Map 是一种用来还原源码位置的技术,记录了打包压缩后代码和源码之间的映射关系,让你在浏览器调试时看到真正的代码。
  2. 常见的环境搭配:

  1. 工作原理:

当在webpack的配置文件中配置了 Source Map,打包文件会产生一个魔法注释。这个魔法注释告诉浏览器,这份文件有 Source Map。浏览器看到这行文件后,会请求对应的 Map 文件,然后使用该 .map文件中的信息,进行还原,映射和调试。

其他常见优化场景:

  1. 开发环境优化

  • 增量构建
  • HMR
  • Source Map
  • 多进程多实例构建: Thread Loader
  1. 生产环境优化

  • 利用CDN
  • Tree shaking
  • 代码分割,拆包优化
  1. 构建服务端渲染SSR

  • 将客户端代码和服务端代码分开,减轻服务器负担
  • 什么是SSR:指的是在服务端产生HTML页面,然后将完整的HTML页面发送到浏览器进行展示的一种渲染方式
  • 与客户端渲染CSR相比,SSR在服务器上运行JS代码生成页面,而不是依赖浏览器在客户端运行代码生成页面
  • SSR的核心流程:

暂时无法在飞书文档外展示此内容

Babel 原理:

  • Babel是什么:

    • Babel就是允许开发者在低版本环境下使用最新的js语法
    • babel就是可扩展的插件系统,用来添加,删除,或转换规则
  • Babel的三种存在方式:

    • 使用单体文件
    • 命令行:就是在package.json的scripts命令中的某条命令
    • 构建工具的插件:webpack的babel-loader
  • 运行方式和插件:

    • Babel总共分为三个阶段:解析,转换,生成
    • Babel本身不具有任何转换功能,他把一个个转换的功能都分解到一个个plugin中,因此当我们不配置任何插件时,经过Babel的代码和输入是相同的
    • 在解析阶段:把源码转换成结构化的AST,在转换阶段:遍历AST转换为目标字符串
  • preset:一个功能一个插件,那岂不是要安装很多插件,设立预设就是提供了一组插件的集合

  • Babel原理:

    • 解析:将代码转换成AST
    •   词法分析:将代码分割成tokens流
    •   语法分析:分析token数组转换成AST树
    • 转换:遍历AST并生成新的AST
    • 生成:以新的AST为基础生成代码
❌
❌