背景

我们在使用 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. 低端hdmi视频采集卡的替代方案——向日葵远控

    前一阵看直播推货入手了一款hdmi视频采集卡,如下: ======================================================= 使用这个视频采集卡发现两个问题: ...

  2. python进程绑定CPU的意义

    1. 绑定CPU后对计算密集型的任务可能会一定程度上提升运算性能:(小幅度的性能提升,甚至小幅度落后,总之就是差别不大) 对比1代码A: import os from multiprocessing ...

  3. WhaleScheduler为银行业全信创环境打造统一调度管理平台解决方案

    项目背景 数字金融是数字经济的重要支撑和驱动力.近年来,我国针对数字金融的发展政策频频出台,<金融科技发展规划 (2022-2025年)>.<"十四五"数字经济发 ...

  4. 如何判断数据库和对象存储是否被 JuiceFS 使用?

    随着使用 JuiceFS 的时间越来越长,一些用户已经用多种数据库和对象存储创建了很多的 JuiceFS 文件系统.有些是纯云端的,有些是纯本地的,有些则是本地与云端结合的.它们当中有一些是存储了文件 ...

  5. 手把手教Linux驱动4-进程、文件描述符、file、inode关系详解

    本文目标 什么是文件描述符? 进程打开文件相关信息管理 Linux设备文件三大结构:inode,file,file_operations mknod 做了什么事? 进程打开设备文件 驱动如何支持同类型 ...

  6. 【CMake系列】07-export与find

    为了将我们的库文件更方便地提供给他人使用,cmake 提供了一种方式,通过查找 .cmake 文件,将库导入项目中. 本节学习的内容,就是将我们的库导出一个 xxx.cmake 文件,以及 在项目中导 ...

  7. C#给类的添加扩展方法

    今天学习了给类添加扩展方法,这里记录下.我这记性不好,过不了两天就给忘记了. 下面的例子是给控件ListView类添加三个方法: 1.  AddItemInofStudent 作用是用结构体数据添加新 ...

  8. iPhone 打不开 Apple News 解决方法

    想看 Apple News,但是在主屏幕找不到,在 App Store 搜索 Apple News 后打开时显示访问控制已启用,然而在设置中检查发现访问控制并没有启用. 经过一番摸索,发现这个访问控制 ...

  9. 使用 SSH 转义代码来控制连接

    OpenSSH 最常被忽视的一个非常有用的功能是能够从连接内部控制会话的某些方面.通过使用 SSH 转义代码,我们能够在会话内部与本地 SSH 软件进行交互. 强制从客户端断开连接(如何退出卡住或冻结 ...

  10. pyinstaller 打包 win32ctypes.pywin32.pywintypes.error: (225, '', '无法成功完成操作,因为文件包含病毒或潜在的垃圾软件。')

    背景: 使用python 写了一个程序,使用pyinstaller打包, 不使用-w --noconsole的命令打包隐藏命令行窗口时,是正常的, 但是使用-w或者--noconsole就会报错win ...