Vite 打包碎片化,如何化解? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
zhennann
V2EX    Node.js

Vite 打包碎片化,如何化解?

  •  1
     
  •   zhennann 2024 年 10 月 14 日 2960 次点击
    这是一个创建于 477 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

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

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

    拆包合并的两种隐患

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

    1. 导致初始包体积过大

    拆包弊端 1.png

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

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

    拆包弊端 2.png

    如图所示,由于文件之间的相互依赖,导致打包后的 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 。要达到这种分包效果,不需要做任何配置。

    chunk-home-base.png

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

    chunk-home-layout.png

    源码分析

    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 。

    两种隐患的解决之道

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

    拆包弊端 3.png

    为了防止两种隐患情况的发生,我们可以实现一种更精细的动态加载和资源定位的机制。简而言之,当我们在模块 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 classRenderFirstPage { render() { return ( <div> <ZCard/> </div> ); } } 

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

    结语

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

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

    3 条回复    2024-10-27 21:40:09 +08:00
    PHSix
        1
    PHSix  
       2024 年 10 月 23 日
    请问什么情况下会出现 2 中描述的循环依赖的情况呢?为啥会分成两个 chunk 之后还互相引用呢
    zhennann
        2
    zhennann  
    OP
       2024 年 10 月 25 日
    @PHSix 如果采用 vite 默认的拆包配置,分成两个 chunk ,自然不会导致互相引用。但是,这会导致大量小文件的产生。为了避免这种碎片化,就需要通过 rollupOptions.output.manualChunks 定制拆包策略,比如把某些文件合成一个 chunk 。在这种情况下,如果配置不当,就会导致循环引用,参见图示的范例。
    PHSix
        3
    PHSix  
       2024 年 10 月 27 日
    @zhennann 明白了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1004 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 22:25 PVG 06:25 LAX 14:25 JFK 17:25
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86