webpack:详解代码分离以及插件SplitChunksPlugin的使用

news2024/11/26 14:44:54

文章目录

    • 背景
    • 入口起点分离
      • 基本使用
      • 防重复
    • SplitChunksPlugin插件分离
      • 背景
      • 基本使用
        • splitChunks.chunks
        • splitChunks.minChunks
        • splitChunks.minSize
        • splitChunks.maxSize
        • splitChunks.name
        • splitChunks.cacheGroups
        • splitChunks.cacheGroups.{cacheGroup}.priority
        • splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk
        • splitChunks.cacheGroups.{cacheGroup}.test
        • splitChunks.cacheGroups.{cacheGroup}.filename
    • 举例
      • 例一
      • 例二
      • 例三

背景

代码分离可以说是 webpack 最牛逼的功能了,使用代码分离可以将 chunks 分离到不同的 bundle 中,比如把不经常更新的库打包到一起放在一个 bundle 中缓存起来,这样可以减少加载时间等等。

什么是 chunks?
英文原意:块,可以被 import、require等引用的模块就是 chunk

什么是 bundle?
英文原意:束,捆,包,把一个或者多个模块打包成的一个整体就叫 bundle,比如我们项目中打包后 dist 中的内容

常用的代码分离方法有两种:

  • 入口起点分离:使用 entry 配置手动地分离代码
  • SplitChunksPlugin插件分离

可能会遇到的问题:

  • 重复问题

代码分离的技巧:

  • 动态导入:通过模块的内联函数调用分离代码。

https://webpack.docschina.org/guides/code-splitting/

入口起点分离

基本使用

我们配置两个入口

const path = require('path');

module.exports = {
 mode: 'development',
 entry: {
   index: './src/index.js',
   another: './src/another-module.js',
 },
  output: {
   filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

其中 index.js 引入了 another-module.js,another-module.js引入了 lodash。
但是打包后我们发现两个文件:

index.bundle.js 553 KiB
another.bundle.js 553 KiB

也就是说如果入口 chunk 之间包含一些重复的模块,那么这些重复模块都会被引入到各个 bundle 中。当然这个也是可以解决的,只需要配置 dependOn 选项就可以防止重复。

防重复

const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
   index: {
     import: './src/index.js',
     // add
     dependOn: 'shared',
   },
   another: {
     import: './src/another-module.js',
     // add
     dependOn: 'shared',
   },
   // add
   shared: 'lodash',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

如果想要在一个 HTML 页面上使用多个入口,还需设置 optimization.runtimeChunk: ‘single’

const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
   index: {
     import: './src/index.js',
     dependOn: 'shared',
   },
   another: {
     import: './src/another-module.js',
     dependOn: 'shared',
   },
   shared: 'lodash',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  // add
  optimization: {
    runtimeChunk: 'single',
  },
};
shared.bundle.js 549 KiB
runtime.bundle.js 7.79 KiB
index.bundle.js 1.77 KiB
another.bundle.js 1.65 KiB

官方并不推荐多入口,既是是多入口,也推荐 entry: { page: ['./analytics', './app'] } 这种写法

SplitChunksPlugin插件分离

背景

最初,chunks(以及内部导入的模块)是通过内部 webpack 图谱中的父子关系关联的。CommonsChunkPlugin 曾被用来避免他们之间的重复依赖,但是不可能再做进一步的优化。

从 webpack v4 开始,移除了 CommonsChunkPlugin,取而代之的是 optimization.splitChunks。所以这个插件是 webpack内置的,不需要单独导入。

基本使用

如果你没有配置 optimization.splitChunks,那么 webpack 会使用这份默认配置。这里配置的目的都是表示什么样的模块可以进行分割打包,比如下面的 minSize 表示大于等于2k的模块才会被分割

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

我们分析一下这些字段代表的含义:

  • 默认只对按需引入的模块进行代码分割;
  • 来自 node_modules 的模块,或被引用两次及以上的模块,才会做代码分割;
  • 被分割的模块必须大于30kb(代码压缩前);
  • 按需加载时,并行的请求数必须小于或等于5;
  • 初始页加载时,并行的请求数必须小于或等于3;

接下来这里只说一些重要的字段含义:

splitChunks.chunks

可选值: function (chunk) | initial | async | all

  • initial 表示入口文件中非动态引入的模块
  • all 表示所有模块
  • async 表示异步引入的模块

动态/异步导入
第一种,符合 ECMAScript 提案 的 import() 语法
第二种,是 webpack 的遗留功能,使用 webpack 特定的 require.ensure

splitChunks.minChunks

拆分前必须共享模块的最小 chunks 数,也就是说如果这个模块被依赖几次才会被分割,默认为1

splitChunks.minSize

生成 chunk 的最小体积,单位为子节,1K=1024bytes

splitChunks.maxSize

同上相反

splitChunks.name

用户指定分割模块的名字,设置为true表示根据chunks和cacheGroup key自动生成

可选值: boolean: true | function (module, chunks, cacheGroupKey) | string

名称可以通过三种方式获取

module.rawRequest
module.resourceResolveData.descriptionFileData.name
chunks.name

使用 chunks.name 获取的时候需要使用 webpack 的魔法注释

import(/*webpackChunkName:"a"*/ './a.js')

举例:

name(module, chunks, cacheGroupKey) {
  // 打包到不同文件中了
  return `${cacheGroupKey}-${module.resourceResolveData.descriptionFileData.name}`;
  // 如果是写死一个字符串,那么多个chunk会被打包到同一个文件中,这样可能会导致首次加载变慢
  // return 'maincommon';
  // 指定打包后的文件所在的目录
  // return 'test/commons';
}
splitChunks.cacheGroups

缓存组可以继承和/或覆盖来自 splitChunks.* 的任何选项。但是 test、priority 和 reuseExistingChunk 只能在缓存组级别上进行配置。将它们设置为 false以禁用任何默认缓存组。

module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        // 默认为 true,表示继承 splitChunks.* 的字段
        default: false,
      },
    },
  },
};
splitChunks.cacheGroups.{cacheGroup}.priority

一个模块可以属于多个缓存组,所以需要优先级。default 组的优先级为负数,我们自定义组的优先级默认为 0

splitChunks.cacheGroups.{cacheGroup}.reuseExistingChunk

如果这个缓存组中的chunk已经在入口模块(main module)中存在了,就不会引入

splitChunks.cacheGroups.{cacheGroup}.test
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        svgGroup: {
          test(module) {
            // `module.resource` contains the absolute path of the file on disk.
            // Note the usage of `path.sep` instead of / or \, for cross-platform compatibility.
            const path = require('path');
            return (
              module.resource &&
              module.resource.endsWith('.svg') &&
              module.resource.includes(`${path.sep}cacheable_svgs${path.sep}`)
            );
          },
        },
        byModuleTypeGroup: {
          test(module) {
            return module.type === 'javascript/auto';
          },
        },
        testGroup: {
		  // `[\\/]` 是作为跨平台兼容性的路径分隔符,也就是/
		  test: /[\\/]node_modules[\\/]/,
		}
      },
    },
  },
};
splitChunks.cacheGroups.{cacheGroup}.filename
module.exports = {
  //...
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          filename: '[name].bundle.js',
          filename: (pathData) => {
            // Use pathData object for generating filename string based on your requirements
            return `${pathData.chunk.name}-bundle.js`;
          },
        },
      },
    },
  },
};

举例

把默认配置放在这里做对照方便查阅

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

例一

// 静态引入
import lodash from 'lodash'
import(/*webpackChunkName:"jquery"*/'jquery')
import('./echarts.js')
console.log('hello world')
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      name(module, chunks, cacheGroupKey) {
        // 打包到不同文件中了
        return `${cacheGroupKey}-${module.resourceResolveData.descriptionFileData.name}`;
      },
    },
  },
};

lodash 是静态引入的,jquery 和 echarts 是动态引入的,而我们这里配置了 async,所以打包会分割出出动态引入的包:

// 可以看到这里的 cacheGroupKey 就是 defaultVendors,也就是默认的分组名称。
defaultVendors-jquery.js
// 主包中包含了lodash和console.log('你好')
main.js
// echarts 也是动态引入的,但是由于走的相对路径,所以name函数无法对其自定义,因为name函数是在外面,只对默认的 defaultVendors 组负责,而这个组中没有对 name 的自定义,所以就生成了默认的。
510.js

例二

那么接下来我们对 echarts 进行分组

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      name(module, chunks, cacheGroupKey) {
        // 打包到不同文件中了
        return `${cacheGroupKey}-${module.resourceResolveData.descriptionFileData.name}`;
      },
      cacheGroups: {
        echartsVendor: {
          test: /[\\/]echarts/,
          name: 'echarts-bundle',
          chunks: 'async',
        },
      },
    },
  },
};
// 可以看到这里的 cacheGroupKey 就是 defaultVendors,也就是默认的分组名称。
defaultVendors-jquery.js
// 主包中包含了lodash和console.log('你好')
main.js
// 由 echartsVendor 组生成的bundle
echarts-bundle.js

例三

打包小程序的时候,如果主包依赖分包的js,会把分包的代码打包进主包的 bundle 里,这里做一下调整也用到了 splitChunks

原本是这样的,可以看到所有的包都打进了 common 里面

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'all',
      name: 'common'
    },
  },
};

修改后

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'all',
      name: 'common',
      cacheGroups: {
        echartsVendor: {
          test: /[\\/]subpackage-echarts[\\/]/,
          name: 'subpackage-echarts/echartsVendor',
          chunks: 'all'
        },
        compontentsVendor: {
          test: /[\\/]subpackage-components[\\/]/,
          name: 'subpackage-components/componentsVendor',
          chunks: 'all',
          minSize: 0
        }
      }
    },
  },
};

可以看到,如果是分包 subpackage-echarts 和 subpackage-components,我会把打包后的 bundle 放到对应的分包文件夹里。但是我觉得这样有点硬编码了,如果后面再增加一个分包还是会有此问题,于是再修改一下

const { resolve } = require('path')
const fs = require('fs')
/**
 * @function 获取分包名称
 * @returns {Array} ['subpackage-a', 'subpackage-a']
 */
const getSubpackageNameList = () => {
  const configFile = resolve(__dirname, 'src/app.json')
  const content = fs.readFileSync(configFile, 'utf8')
  let config  = ''
  try {
    config = JSON.parse(content)
  } catch (error) {
    console.log(configFile)
  }

  const { subpackages } = config
  return subpackages.map(item => item.root)
}

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'all',
      name: 'common',
      cacheGroups: {
        subVendor: {
          test: (module) => {
            const list = getSubpackageNameList()
            const isSubpackage = list.some(item => module.resource.indexOf(`/${item}/`) !== -1)
            return isSubpackage
          },
          name(module, chunks, cacheGroupKey) {
            const list = getSubpackageNameList()
            const subpackageName = list.find(item => module.resource.indexOf(`/${item}/`) !== -1)
            return `${subpackageName}/vendor`
          },
          chunks: 'all',
          minSize: 0
        },
      }
    },
  },
};

在这里插入图片描述

可以看到打包后依赖分包的文件都放到了分包里,这样,不管后面怎么增加分包,都不用修改代码了。

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

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

相关文章

智能制造,RFID与流转线碰撞

智能制造,RFID与流转线碰撞 RFID是一种无线通信技术,可以用于实时识别和物品。它通过将包含有信息的标签贴在物品上,利用射频信号进行通信,将物品的信息传输给读取器。读取器可以通过无线方式读取标签上的信息,从而实…

公众环境关注如何提升企业ESG表现(2011-2022年)

参照陶云清(2023)的做法,本团队对来自“科学学与科学技术管理”《公众环境关注如何提升企业ESG表现?—基于外部压力与内部关注的双重视角》一文中的基准回归部分进行复刻。 如何驱动企业在逐利过程中兼顾环境、社会和治理表现&am…

IC业务杂谈-----片内外设、片上外设 和、片外外设的区别

片内外设就是片上外设,同一种意思不同说法而已。 片内外设和片外外设的区别: 片内、外设是两个概念,片内指做成芯片的集成电路内部,简称片内,片外同理显而易见;外设是外部设备的简称,是指集成…

Direct3D融合技术

该技术能使我们将当前要进行光栅化的像素的颜色与先前已已光栅化并处于同一位置的像素的颜色进行合成,即将正在处理的图元颜色值与存储在后台缓存中的像素颜色值进行合成(混合),利用该技术我们可得到各种各样的效果,尤其是透明效果。 在融合…

根据经纬度数据画出船只航行路径图(cartopy + matplotlib)

问题: 已知穿航行的经纬度记录,怎么在地图上画出? 思路: 1先画出地图,使用cartopy。 2然后再将不连续的点绘制在地图中,这样的连线就是轨迹了。 ( cartopy库的安装见我的其他文章简明cartop…

【postgresql 】 ERROR: “name“ is not supported as an alias

org.postgresql.util.PSQLException: ERROR: "name" is not supported as an alias 错误:不支持将“name”作为别名 SELECT real_name name FROM doc_user 加上 在关键词上加上 “” 示例: SELECT real_name "name" FROM do…

05_2D3D转换

1 2D 转换 转换是 CSS3 中具有颠覆性的一个特征,可以实现元素的位移、旋转、变形、缩放。 通过 transform 转换来实现 2D 转换或者 3D 转换。 2D 转换包括: 缩放 scale移动 translate旋转 rotate倾斜 skew(了解) 1.1 缩放 sc…

【EI会议信息】第五届物联网、自动化和人工智能国际会议(IoTAAI 2023)

第五届物联网、自动化和人工智能国际会议(IoTAAI 2023) 2023 5th International Conference on Internet of Things, Automation and Artificial Intelligence 第五届物联网、自动化和人工智能国际会议(IoTAAI 2023)将于2023年1…

周界警戒AI算法+视频智能分析在安全生产场景中的应用

长期以来,周界防范安防系统在大型园区、工厂、社区、机场、火车站站台、重点单位等领域应用较为广泛和常见。随着AI人工智能等新兴技术的快速发展与落地应用,通过AI智能检测与视频智能分析技术,现代化的周界安防系统可以做到全天候快速、准确…

【PX4】PX4第一个offborad例程

【PX4】PX4第一个offborad例程 文章目录 【PX4】PX4第一个offborad例程1. 什么是OFFBOARD2. 第一个offboard例程3. 编写launch文件Reference 1. 什么是OFFBOARD PX4的OFFBOARD指的是外部控制模式,飞行器根据飞行控制栈外部(如机载计算机)提供…

C/C++/Python图像处理算法实战【3】彩色图像灰度化和二值化处理

本篇文章旨在通过详细的代码逐行注释,介绍如何分别使用 C/C++/Python 对彩色图像做灰度化(最大值法、平均值法、加权平均值法)和二值化处理(平均值法、双峰法、OTSU法)。 原理解析 图像表示 数字图像通常采用矩阵表示。以一幅数字图像F左上角像素中心为坐标原点,一幅M…

Prometheus-Rules 实战

文章目录 1 node rules2 nginx rule2.1 Nginx 4xx 错误率太多2.2 Nginx 5xx 错误率太多2.3 Nginx 延迟高 3 mysql rule3.1 MySQL 宕机3.2 实例连接数过多3.3 MySQL高线程运行3.4 MySQL 从服务器 IO 线程没有运行3.5 MySQL 从服务器 SQL 线程没有运行3.6 MySQL复制滞后3.7 慢查询…

小米手机打开开发者模式

1、打开设置 2、 3、 4、多次连续点击版本,直到提示打开开发者模式 5、进入手机开发者模式后,点击进入“设置”主页的“更多设置”。 6、接着点击进入“开发者选项”。 7、最后打开“USB调试”选项后,手机就打开了USB调试模式。 8、可以…

飞行动力学 - 第35节-动操纵性 之 基础点摘要

飞行动力学 - 第35节-动操纵性 之 基础点摘要 1. 动操纵性2. 传递函数3. 动稳定性与动操纵性4. 参考资料 1. 动操纵性 Free response: x 0 ≠ 0 , u 0 x_0 \ne 0, u 0 x0​0,u0Forced response: x 0 0 , u ≠ 0 x_0 0, u \ne 0 x0​0,u0 驾驶员操纵飞机的方式有开环…

【.net core】解决无法下载wgt文件问题

//StartUp.cs文件中Configure方法中添加以下代码 app.UseStaticFiles(new StaticFileOptions{FileProvider new PhysicalFileProvider(Directory.GetCurrentDirectory()),ContentTypeProvider new FileExtensionContentTypeProvider(new Dictionary<string, string>{{ …

高通recovery流程分析(编译、界面、图片)

目录 recovery 界面菜单 recovery 界面操作 recovery 启动流程 recovery 编译makefile recovery 图片大小 ramdisk、boot.img、recovery.img之间的关系 authordaisy.skye的博客_CSDN博客-嵌入式,Qt,Linux领域博主 recovery 界面菜单 recovery 界面显示 android recoveryuse …

酒店布草管控RFID智能化回收管理

酒店行业作为服务业的重要组成部分&#xff0c;拥有大量的布草资产&#xff0c;如客房被罩、床单、浴巾和毛巾等&#xff0c;为了更好地管理和追踪这些布草的使用情况&#xff0c;提高效率和准确性&#xff0c;酒店多采用RFID技术进行布草智能化回收管理。 RFID电子标签的应用…

【深度学习】LeNet网络架构

文章目录 什么是LeNet代码实现网络架构 什么是LeNet LeNet是一种经典的卷积神经网络&#xff0c;由Yann LeCun等人在1998年提出。它是深度学习中第一个成功应用于手写数字识别的卷积神经网络&#xff0c;并且被认为是现代卷积神经网络的基础。 LeNet模型包含了多个卷积层和池…

JOSEF约瑟 剩余电流继电器PFR-5 PFE-W-20 国产化改造ZLR-G81 ZCT-45

系列型号&#xff1a; PFR-003剩余电流继电器 PFR-03剩余电流继电器 PFR-5剩余电流继电器 PFR-W-105互感器 PFR-W-140互感器 PFR-W-20互感器 PFR-W-210互感器 PFR-W-30互感器 PFR-W-35互感器 PFR-W-70互感器 一、用途 PFR剩余电流继电器&#xff08;以下简称继电器…

【教学类】小2班学号字帖(A4横版2份)

图片展示: 背景需求: 突然接到通知&#xff0c;明天下午临时去带小2班。 小班刚入园的孩子&#xff0c;能给他们提供什么样的可操作的学具呢&#xff1f; 思来想去&#xff0c;还是让生成一份学号字帖&#xff0c;让幼儿熟悉自己的学号&#xff0c;让我也熟悉幼儿的名字和学…