背景

我们在使用 Vite 进行打包时,经常会遇到这个问题:随着业务的展开,版本迭代,页面越来越多,第三方依赖也越来越多,打出来的包也越来越大。如果把页面都进行动态导入,那么凡是几个页面共用的文件都会进行独立拆包,从而导致大量 chunk 碎片的产生。许多 chunk 碎片体积都很小,比如:1k,2k,3k,从而显著增加了浏览器的资源请求。

虽然可以通过rollupOptions.output.manualChunks定制分包策略,但是文件之间的依赖关系错综复杂,分包配置稍有不慎,要么导致初始包体积过大,要么导致出现循环依赖错误,因此心智负担很重。那么有没有自动化的分包机制来彻底解决打包碎片化的问题呢?

拆包合并的两种隐患

前面提到使用rollupOptions.output.manualChunks定制分包策略有两种隐患,这里展开说明一下:

1. 导致初始包体积过大

如图所示,文件 A 本来只依赖文件 C,但是按照图中所示分包配置,导致在使用文件 A 之前必须先下载 Chunk1 和 Chunk2。在稍微大一点的项目中,由于文件之间的依赖关系非常复杂,这种依赖关系会随着大量小文件的合并而快速蔓延,导致初始包体积过大。

2. 导致出现循环依赖错误

如图所示,由于文件之间的相互依赖,导致打包后的 Chunk1 和 Chunk2 出现循环依赖的错误。那么在复杂的项目中,业务之间相互依赖的情况就更加常见。

解决之道:模块化体系

由于分包配置会导致以上两个隐患,所以往往步履维艰,很难有一个可以遵循的简便易用的配置规则。因为分包配置与业务的当前状态密切相关。一旦业务有所变更,分包配置也需要做相应的改变。

为了解决这个难题,我在项目中引入了模块化体系。也就是将项目的代码依据业务特点进行拆分,形成若干个模块的组合。每一个模块都可以包含页面、组件、配置、语言、工具等等资源。然后一个模块就是一个天然的拆包边界,在 build 构建时,自动打包成一个独立的异步 chunk,告别 Vite 配置的烦恼,同时可以有效避免构建产物的碎片化。特别是在大型业务系统中,这种优势尤其明显。当然,采用模块化体系也有利于代码解耦,便于分工协作。

由于一个模块就是一个拆包边界,我们可以通过控制模块的内容和数量来控制产物 chunk 的大小和数量。而模块划分的依据是业务特点,具有现实的业务意义,相较于rollupOptions.output.manualChunks定制,显然心智负担很低。

文件结构

随着项目不断迭代演进,创建的业务模块也会随之膨胀。对于某些业务场景,往往需要多个模块的配合实现。因此,我还在项目中引入了套件的概念,一个套件就是一组业务模块的组合。这样,一个项目就是由若干套件和若干模块组合而成的。下面是一个项目的文件结构:

project
├── src
│ ├── module
│ ├── module-vendor
│ ├── suite
│ │ ├── a-demo
│ │ └── a-home
│ │ ├── modules
│ │ │ ├── home-base
│ │ │ ├── home-icon
│ │ │ ├── home-index
│ │ │ └── home-layout
│ └── suite-vendor
名称 说明
src/module 独立模块(不属于套件)
src/module-vendor 独立模块(来自第三方)
src/suite 套件
src/suite-vendor 套件(来自第三方)
名称 说明
a-demo 测试套件:将测试代码放入一个套件中,从而方便随时禁用
a-home 业务套件:包含 4 个业务模块

打包效果

下面就来看一下实际的打包效果:

以模块home-base为例,图左显示的就是该模块的代码,图右显示的就是该模块打包后的文件体积 12K,压缩后是 3K。要达到这种分包效果,不需要做任何配置。

再比如,我们还可以把布局组件集中放入模块home-layout进行管理。该模块打包成独立的 Chunk,体积为 29K,压缩后是 6K。

源码分析

1. 动态导入模块

由于项目的模块目录结构都是有规律的,我们可以在项目启动之前提取所有的模块清单,然后生成一个 js 文件,集中实现模块的动态导入:

const modules = {};
...
modules['home-base'] = { resource: () => import('home-base')};
modules['home-layout'] = { resource: () => import('home-layout')};
...
export const modulesMeta = { modules };

由于所有模块都是通过 import 方法动态导入的,那么在进行 Vite 打包时就会自动拆分为独立的 chunk。

2. 拆包配置

我们还需要通过rollupOptions.output.manualChunks定制拆包配置,从而确保模块内部的代码统一打包到一起,避免出现碎片化文件。

const __ModuleLibs = [
/src\/module\/([^\/]*?)\//,
/src\/module-vendor\/([^\/]*?)\//,
/src\/suite\/.*\/modules\/([^\/]*?)\//,
/src\/suite-vendor\/.*\/modules\/([^\/]*?)\//,
]; const build = {
rollupOptions: {
output: {
manualChunks: (id) => {
return customManualChunk(id);
},
},
},
}; function customManualChunk(id: string) {
for (const moduleLib of __ModuleLibs) {
const matched = id.match(moduleLib);
if (matched) return matched[1];
}
return null;
}

通过正则表达式匹配每一个文件路径,如果匹配成功就使用相应的模块名称作为 chunk name。

两种隐患的解决之道

如果模块之间相互依赖,那么也有可能存在前面所言的两种隐患,如图所示:

为了防止两种隐患情况的发生,我们可以实现一种更精细的动态加载和资源定位的机制。简而言之,当我们在模块 1中访问模块 2的资源时,首先要动态加载模块 2,然后找到模块 2 的资源,返回给使用方。

比如,在模块 2 中有一个 Vue 组件Card,模块 1 中有一个页面组件FirstPage,我们需要在页面组件FirstPage中使用Card组件。那么,我们需要这样来做:

// 动态加载模块
export async function loadModule(moduleName: string) {
const moduleRepo = modulesMeta.modules[moduleName];
return await moduleRepo.resource();
} // 生成异步组件
export function createDynamicComponent(moduleName: string, name: string) {
return defineAsyncComponent(() => {
return new Promise((resolve) => {
// 动态加载模块
loadModule(moduleName).then((moduleResource) => {
// 返回模块中的组件
resolve(moduleResource.components[name]);
});
});
});
}
const ZCard = createDynamicComponent('模块2', 'Card');

export class RenderFirstPage {
render() {
return (
<div>
<ZCard />
</div>
);
}
}

高级导入机制

虽然使用createDynamicComponent可以达到预期的目的,但是,代码不够简洁,无法充分利用 Typescript 提供的自动导入机制。我们希望仍然像常规的方式一样使用组件:

import { ZCard } from '模块2';

export class RenderFirstPage {
render() {
return (
<div>
<ZCard />
</div>
);
}
}

这样的代码,就是静态导入的形式,就会导致模块 1模块 2强相互依赖。那么,有没有两全其美的方式呢?有的。我们可以开发一个 Babel 插件,对 AST 语法树进行解析,自动将 ZCard 的导入改为动态导入形式。这样的话,我们的代码不仅简洁直观,而且还可以实现动态导入,规避分包时两种隐患的发生。为了避免主题分散,Babel 插件如何开发不在这里展开,如果感兴趣,可以直接参考源代码:babel-plugin-zova-component

结语

本文对 Vite 打包碎片化的成因进行了分析,并且提出了模块化体系,从而简化分包配置,同时又采用动态加载机制,完美规避了分包时两种隐患的发生。

当然,实现一个完整的模块化系统,需要考虑的细节还有很多,如果想体验开箱即用的效果,可以访问我开源的 Zova.js 框架:https://github.com/cabloy/zova。可添加我的微信,入群交流:yangjian2025

Vite打包碎片化,如何化解?的更多相关文章

  1. [No000034]知乎-长期接收碎片化知识有什么弊端?

    你所接受的一切信息,构成了你的思维方式. 所以,长期接受碎片信息的后果,就是让你的思维变得狭隘,难以进行复杂的思考. 碎片信息通常具备这样的特征: •它们往往是一些事实的集合而非逻辑 •它们往往大量简 ...

  2. 腾讯优测优分享 | Android碎片化问题小结——关于闪光灯的那些事儿

    腾讯优测是专业的android自动化测试平台,拥有上千款真机,彻底解决android碎片化问题! 这里我要说的不是闪光灯的硬件特征,也不是说底层驱动的原理,我只是跟大家聊一聊在项目中遇到的一些关于闪光 ...

  3. 程序员MM的自白:磨人小妖精之安卓碎片化

    文/腾讯优测 章婉霞 除了crash问题,Android平台的碎片化越来越受到移动开发的关注,且不谈支持Android系统的移动设备早已过万款,屏幕.品牌以及传感器等方面的碎片化问题也困扰着开发者. ...

  4. 碎片化时代:O2O服务成最大趋势

    从86年第一封电子E-Mail发出至今28个年头,中国互联网从单一点对点,点对多,多对多,如今又回归到点对点,从最基础的Mail.黄页应用到基于互动的Search.Blog以及基于满足人类基本需求的沟 ...

  5. Android 的平台碎片化问题

    Android 的平台碎片化问题 看到篇不错的文章,转载过来. -------------------------------------- 与iOS开发相比,Android开发平添了不小的工作量,因 ...

  6. 计算机内存碎片(中)——外部碎片化(内存 & 文件系统 & 数据库系统通杀)

    本文原创为freas_1990,转载请标明出处:http://blog.csdn.net/freas_1990/article/details/17252221 外部碎片化 当计算机内存被划分成很细碎 ...

  7. 一个使用openGL渲染的炫丽Android动画库二(碎片化曲面动画)

    续一个使用openGL渲染的炫丽Android动画库 MagicSurfaceView v1.1.0发布, 新增碎片化曲面动画 地址:https://github.com/gplibs/android ...

  8. 利用碎片化时间Get Linux系统

    利用碎片化时间Get Linux系统 起初,我做着一份与IT毫无关系的工作,每月领着可怜的工资,一直想改变现状,但无从下手,也就是大家熟知的迷茫.我相信,每一个人都会或多或少的经历过迷茫,迷茫每一个选 ...

  9. 用 Core Animation 实现图片的碎片化

    用 Core Animation 实现图片的碎片化 参考书籍: 效果如下: 原理其实非常简单哦:). 1. 创建一个CALayer,使用其 contents 属性来装载一张图片(获取图片的CGImag ...

  10. 图片碎片化mask动画

    图片碎片化mask动画 效果 源码 https://github.com/YouXianMing/Animations // // TransformFadeViewController.m // A ...

随机推荐

  1. 【JS】06 语法补充

    严格模式(use strict) 其实就是就是对JS随意的语法做一个强制规范要求 开启严格模式: "use strict"; 注意,只有在第一行声明才会有效,. 否则在严格模式之前 ...

  2. 乌克兰学者的学术图谱case5

    ========================================== 背景: 弗兰采维奇材料问题研究是欧洲最大的材料科研院所,在核电.航空.航天.军工及其他装备制造领域的先进材料研制方 ...

  3. &times被替换成x 的解决办法

    今天写代码遇到一个很有趣的问题: 在php中使用echo 输出url的时候当url中包含&times字段时就会被html直接解析成 x (乘号)这样一来我返回的地址就不能正常访问url了: 解 ...

  4. 旧物利用 - 将机顶盒改造为一台Linux开发机!

    前言 机顶盒型号:移动魔百盒CM201-2(CH),芯片组: hi3798mv300(hi3798mv3dmm),其他型号类似 理论上适用于以下SOC:Hi3798Mv100 / Hi3798Cv20 ...

  5. 这就是为什么你学不会DDD

    本文书接上回<为了给Javaer落地DDD,我们不得不写开源组件>,欢迎关注公众号(老肖想当外语大佬),获取最新文章更新和DDD框架源码,视频和直播在B站. https://mp.weix ...

  6. DRF中serializer的中的模型字段解释

    序列化器--Serializer 选项参数: max_length 最大长度 min_length 最小长度 allow_blank 是否允许为空 trim_whitespace 是否截断空白字符 m ...

  7. jxls导入excel

    我们在开发中经常用jxls实现导出功能,殊不知jxls也有导入功能,下面来介绍下如何使用jxls导入excel. 首先在maven的pom中添加相关依赖,如下: <dependency> ...

  8. .NET8 Blazor 从入门到精通:(三)类库和表单

    目录 Razor 类库 创建 使用 使可路由组件可从 RCL 获取 静态资源 表单 EditForm 标准输入组件 验证 HTML 表单 Razor 类库 这里只对 RCL 创建和使用的做一些简单的概 ...

  9. JavaScript设计模式样例十四 —— 观察者模式

    观察者模式(Observer Pattern) 定义:当一个对象被修改时,则会自动通知它的依赖对象.目的:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被 ...

  10. in notin exists not exists 性能优化算法总结

    in notin exists not exists 性能优化算法总结 1.1. in 和 exists 区别 1.2. not in 能不能走索引 1.3. not in 和 join 的关系 1. ...