上个月我们的小程序突然提交审核失败,微信开发者工具直接报“主包体积超过2M”。当时项目已经跑了快一年,功能越堆越多,各种第三方图表库、业务模块都挤在pages目录下。老板说必须三天内解决,否则影响产品发布。我花了两个通宵把项目从单包架构改成了“主包+多个独立分包”的异步化方案,最终主包从2.3M降到了1.1M,审核顺利通过。这个过程踩了不少坑,我觉得有必要把独立分包的正确打开方式完整梳理一遍。
一、先搞清楚:普通分包和独立分包的区别
uniapp里配置分包并不新鲜,在pages.json里加个subPackages就能把一些页面挪到子包里。但普通子包有一个关键限制:它依赖主包的公共资源,比如公共组件、JS库、样式文件都被打包在主包里。这意味着子包里的页面在加载时,主包必须已经存在,而且子包自身不能独立运行。
独立分包是微信小程序原生支持的一种特殊分包形式。它在subPackages配置里加上"independent": true之后,这个子包就变成了一个完全自治的单元。它可以拥有自己独立的静态资源、组件和公共逻辑,甚至可以在主包还没加载的情况下独立打开。对于功能庞大、模块间耦合度低的应用来说,独立分包是真正的“拆包利器”。
不过微信对独立分包的规则比较严格:独立分包不能依赖主包或其它分包的资源,包括components、js模块、图片等,所有依赖都必须自包含。同时独立分包之间的页面跳转需要使用特定的API,不能直接写路径。
二、项目改造:从单包到多独立分包
我们的项目原本结构大致如下,所有功能模块的页面都直接放在pages目录里:
├── pages/
│ ├── user/ # 用户相关页面
│ ├── order/ # 订单相关页面
│ ├── statistics/ # 数据统计页面(依赖图表库)
│ └── settings/ # 设置页面
├── components/ # 全局公共组件
├── utils/ # 公共工具函数
└── static/ # 静态资源
改造的第一步是识别哪些模块适合做成独立分包。我的判断标准是:该模块功能完整、页面自成体系、不频繁与其它模块进行复杂的数据交互。按照这个标准,我把statistics和settings分别改成了独立分包,user和order因为经常有互相跳转和数据共享,暂时保留在主包里作为普通子包。
在pages.json中配置如下:
{
"pages": [
{
"path": "pages/user/user",
"style": { "navigationBarTitleText": "个人中心" }
},
{
"path": "pages/order/list",
"style": { "navigationBarTitleText": "订单列表" }
}
],
"subPackages": [
{
"root": "subpkg-statistics",
"pages": [
{
"path": "pages/dashboard",
"style": { "navigationBarTitleText": "数据看板" }
},
{
"path": "pages/report",
"style": { "navigationBarTitleText": "报表" }
}
],
"independent": true
},
{
"root": "subpkg-settings",
"pages": [
{
"path": "pages/settings",
"style": { "navigationBarTitleText": "设置" }
}
],
"independent": true
}
]
}
注意我把独立分包的根目录命名为了subpkg-statistics和subpkg-settings,这是为了在代码里一眼就能识别出它是个独立分包,方便后期维护。配置完成后,对应的页面文件夹也要从pages下移到这些新目录里。
接下来处理依赖。统计模块原本依赖一个全局的图表工具类utils/chartHelper.js,现在独立分包不能引用主包的utils,我必须把这个工具类复制一份放到subpkg-statistics目录内。虽然这会造成一点代码冗余,但对于独立分包来说,自包含的好处远远大于这点重复。复制的文件结构如下:
subpkg-statistics/
├── pages/
│ ├── dashboard.vue
│ └── report.vue
├── utils/
│ └── chartHelper.js # 从主包复制过来的图表工具
├── components/
│ └── ChartPanel.vue
└── static/ # 独立分包的静态资源
同样,设置模块也复制了它专属的工具函数和组件。主包因此瘦身不少,原本全局引用的图表库(echarts的min版本)因为只在统计模块使用,直接移到了独立分包内,主包体积一下子就减了接近500KB。
三、异步化加载:让独立分包真正按需出现
独立分包配置好了,但用户进入小程序时如果直接加载主包页面,独立分包并不会被下载。只有当用户首次访问独立分包里的页面时,微信才会触发分包的异步下载。这种按需加载本身就是异步化的体现,不需要手动写加载逻辑。
但问题在于,从主包页面跳转到独立分包页面,或者从独立分包A跳转到独立分包B时,不能简单地使用uni.navigateTo({ url: '/subpkg-statistics/pages/dashboard' }),因为独立分包之间是隔离的。正确的做法是利用微信小程序的分包异步化能力。在uniapp里,跳转到独立分包页面可以直接使用绝对路径,框架会处理分包加载的过程。但如果涉及到跨分包的组件或方法调用,就需要用到require.async或者uniapp提供的动态引入。
举个例子,统计模块里有一个生成报表的函数,设置模块偶尔也需要调用它来导出一份配置数据。由于两个独立分包不能直接互相引用,我就在公共的全局层(主包)定义了一个事件总线式的调用接口,利用uni.$emit和uni.$on实现松耦合通信。但更好的做法是把这个共享功能抽取成一个独立分包插件或者放到主包里(如果体积允许)。
实际开发中,我更倾向于让独立分包尽量自闭环。如果确实有跨分包通信的必要,可以在主包中注册一个全局的JS桥接模块,通过异步加载分包的特定页面后传递参数。比如使用以下方式动态跳转并传参:
// 在主包页面中,预先检查独立分包是否存在,然后跳转
uni.navigateTo({
url: '/subpkg-statistics/pages/dashboard?from=main',
success() {
console.log('独立分包加载并跳转成功');
},
fail(err) {
uni.showToast({ title: '模块加载失败,请稍后重试', icon: 'none' });
}
});
如果需要从独立分包里获取一些计算结果返回给主包,可以在独立分包页面里使用uni.$emit在页面卸载前发送事件,主包页面监听该事件。但这种通信方式要求主包页面此时仍然存在,适用于navigateBack的场景。
四、预加载策略:让用户体验更丝滑
独立分包的异步下载需要时间,在网络较慢的环境下,用户点击统计入口后可能要等1-2秒才能看到页面。为了减少等待感,可以在主包配置里加入分包预加载规则。在pages.json中增加preloadRule字段:
{
"preloadRule": {
"pages/user/user": {
"network": "all",
"packages": ["subpkg-statistics"]
},
"pages/order/list": {
"network": "wifi",
"packages": ["subpkg-settings"]
}
}
}
这段配置表示:当用户进入“个人中心”页面时,无论网络环境如何,后台都会默默预下载subpkg-statistics这个独立分包;而当用户访问“订单列表”时,仅在WiFi环境下预下载subpkg-settings。这样在用户真正点击进入统计或设置页面时,分包大概率已经就绪,体验接近于主包页面。
这里有一个细节值得注意:预加载会消耗一定的流量和下载时间,所以不要一股脑把所有独立分包都设置为无条件预加载。应该根据用户行为数据分析,优先预加载那些访问概率高的模块。我在实际项目里统计了近一周的用户路径,发现从“个人中心”点击“数据看板”的概率超过60%,因此将统计分包设为全网络预加载,而设置分包的访问率不到20%,就只在WiFi下预加载。
五、开发中遇到的几个顽固问题
整个改造过程并不是一帆风顺的,有几个问题让我反复调试了很久:
1. 独立分包内的路径引用错误。 在复制文件到独立分包时,部分组件里的图片引用使用了/static/xxx.png这样的绝对路径,这个路径指向的是主包的static目录。独立分包运行时解析不到这些图片,导致页面白屏。解决办法是把图片也复制到独立分包的static目录下,并改为相对路径../../static/xxx.png或者使用~@/static/xxx.png(在vue文件里会自动解析为分包根目录)。
2. 公共样式污染。 在App.vue里定义的全局样式默认会影响到所有分包,包括独立分包。但如果独立分包内的组件使用了相同的类名并且想覆盖全局样式,权重计算可能会变得混乱。我最终在独立分包的页面里使用scoped样式,并尽量避免与主包样式产生强依赖。
3. uni-app的编译差异。 在H5端测试的时候,独立分包的概念不存在,所有页面都会被打包到一起,所以开发时看不出问题。但一编译到微信小程序,路径不对或者依赖缺失就会直接报错。建议每完成一个独立分包的改造,都立即编译到微信开发者工具里验证一下,别等到最后一起调试。
六、体积优化的额外收益
改造完成后,我不但解决了主包超限的问题,还额外获得了一些性能收益。主包体积缩小后,小程序首次启动下载的资源量减少了,冷启动时间从原来的平均2.8秒降到了1.9秒(公司内部几台测试机数据)。独立分包的加载虽然需要额外时间,但因为它是按需触发的,对首屏完全没有影响。更重要的是,现在每个独立分包可以由不同的开发人员负责,代码仓库的模块划分也变得更清晰,不再出现多人同时修改主包导致的冲突问题。
如果你也在维护一个日益臃肿的uniapp小程序项目,强烈建议尽早规划分包架构。不必一步到位,可以先挑一个功能相对独立、体积较大的模块尝试独立分包改造,跑通流程后再逐步迁移其他模块。整个过程对业务侵入性不大,但能带来立竿见影的体积优化和更合理的工程结构。

