uni-app 小程序分包限制处理与主包体积优化实战
本文基于实际项目经验,总结在 uni-app 开发微信小程序过程中遇到的分包限制问题及解决方案,涵盖主包体积超标、跨分包引用限制、延迟加载模式、UMD 模块适配等核心问题。
一、背景:微信小程序的分包机制与限制
微信小程序对包体积有严格限制:
| 限制项 | 限制值 |
|---|---|
| 主包体积 | ≤ 2MB |
| 单个分包体积 | ≤ 2MB |
| 总包体积 | ≤ 20MB |
1.1 分包引用规则
微信小程序的分包引用有严格的限制:
diff
✅ 允许:
- 主包 → 主包资源
- 分包 → 主包资源
- 分包 → 同分包内资源
❌ 禁止:
- 主包 → 分包资源(require)
- 分包A → 分包B资源(require)
关键点 :页面间的 navigateTo 跳转不受限制,限制的是 JS 模块的 require/import。
1.2 常见报错
当违反分包引用规则时,会出现如下错误:
javascript
Error: module 'pagesStrategy/common/vendor.js' is not defined,
require args is './vendor.js'
这种错误在 H5 端不会出现,只在小程序端发生,因为 H5 没有分包概念。
二、主包体积超标的解决方案
2.1 问题分析
当主包体积超过 2MB 限制(例如超标 200KB),需要从以下几个方面入手:
- 页面分包化:将非 tabBar 页面迁移到分包
- 组件分包化:将仅分包使用的组件迁移到对应分包
- 第三方库延迟加载:将大型第三方库通过动态 import 移到分包
- Tree-shaking 优化:使用按需引入替代全量引入
2.2 分包延迟加载模式(核心方案)
这是解决主包体积超标最有效的方案。原理是:
scss
主包组件 --动态 import()--> 分包模块
↑ 编译为 require.async()
↑ 模块代码留在分包,不进入主包 vendor.js
实际案例:埋点 SDK 延迟加载
项目中埋点 SDK(qt_mini.esm.js,122KB)通过以下方式实现延迟加载:
js
// src/utils/aplus.js
const aplusProxy = new Proxy({}, {
get(_, method) {
if (_realAplus) {
const target = _realAplus[method]
return typeof target === 'function' ? target.bind(_realAplus) : target
}
// SDK 未加载时缓存调用
return (...args) => {
_pendingCalls.push({ method, args })
}
}
})
// 动态加载 SDK(编译为 require.async)
import('@/pagesAplus/qt_mini.esm')
.then((QT1) => {
_realAplus = QT1.initQTSDK(config).ctx.aplus
// 回放缓存的调用
_pendingCalls.forEach(({ method, args }) => {
_realAplus[method]?.apply(_realAplus, args)
})
_pendingCalls.length = 0
})
export default aplusProxy
效果 :SDK 代码留在 pagesAplus 分包,主包 vendor.js 不包含 SDK 代码。
实际案例:ECharts 延迟加载
ECharts(按需引入后约 363KB)同样采用此模式:
js
// src/pagesBigJs/echarts.js(在分包中)
import * as echarts from 'echarts/core'
import { LineChart, BarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
GridComponent, TooltipComponent,
LineChart, BarChart, CanvasRenderer
])
export default echarts
js
// src/components/charts/LineChart.vue(在主包)
const refreshChart = async () => {
// 动态加载 echarts(编译为 require.async)
const { default: echarts } = await import('@/pagesBigJs/echarts')
if (!chartInstance) {
chartInstance = await chartRef.value.init(echarts)
}
chartInstance.setOption(buildOption(), true)
}
效果对比:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 主包 vendor.js | 1500 KB | 962 KB |
| 主包总体积 | 2064 KB | 1512 KB |
| 余量 | -52 KB(超标) | +536 KB |
三、分包开发中的常见陷阱
3.1 陷阱一:组件标签名与 import 名不匹配
问题现象:H5 正常渲染,小程序端图表不显示,无报错。
根因 :uni-app 小程序编译器不支持 kebab-case 与下划线的自动转换。
vue
<!-- ❌ 错误:import 名是 l_echart,模板用 l-echart -->
<script setup>
import l_echart from './l-echart/l-echart.vue'
</script>
<template>
<l-echart ref="chartRef" /> <!-- 小程序端不渲染 -->
</template>
<!-- ✅ 正确:标签名与 import 名完全一致 -->
<script setup>
import l_echart from './l-echart/l-echart.vue'
</script>
<template>
<l_echart ref="chartRef" /> <!-- 两端都正常 -->
</template>
验证方法 :检查编译输出的 .json 文件,确认 usingComponents 是否包含组件注册。
3.2 陷阱二:UMD 模块在 Vite/Rollup 中的导入问题
问题现象 :import * as echarts 后 echarts.graphic 为 undefined。
根因:UMD 格式无法被 Vite/Rollup 静态分析命名导出,tree-shaking 会误删导出。
解决方案演进:
| 阶段 | 方案 | 问题 |
|---|---|---|
| V1 | UMD 全量包 + Vite 插件注入 export default |
包体积大,无法 tree-shaking |
| V2 | npm 包 + echarts/core 按需引入 |
包体积减小,但进入主包 |
| V3 | npm 包 + 分包延迟加载 | 最优解 |
最终方案代码:
js
// 分包中的 echarts.js
import * as echarts from 'echarts/core'
import { LineChart, BarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
GridComponent, TooltipComponent, LegendComponent,
LineChart, BarChart,
LabelLayout, UniversalTransition,
CanvasRenderer
])
export default echarts
3.3 陷阱三:渐变色依赖 echarts 实例
问题 :echarts.graphic.LinearGradient 需要 echarts 实例,但延迟加载时 echarts 还未加载。
解决方案:使用纯对象描述渐变色(echarts 支持):
js
// ❌ 依赖 echarts 实例
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#337FD5' },
{ offset: 1, color: '#fff' }
])
// ✅ 纯对象描述
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: '#337FD5' },
{ offset: 1, color: '#fff' }
]
}
四、分包架构设计最佳实践
4.1 目录结构规划
bash
src/
├── components/ # 主包共享组件(体积小)
│ ├── charts/
│ │ ├── LineChart.vue # 图表组件壳
│ │ ├── BarChart.vue
│ │ └── l-echart/ # canvas 适配层
│ └── ...
├── pagesBigJs/ # 大型 JS 库分包
│ ├── echarts.js # echarts 按需引入封装
│ ├── qt_mini.esm.js # 埋点 SDK
│ └── placeholder/ # 分包占位页
├── pages/ # 主包页面(tabBar 页等)
├── pagesOrder/ # 订单分包
├── pagesService/ # 服务详情分包
└── ...
4.2 分包命名规范
| 分包名 | 用途 | 内容 |
|---|---|---|
pagesBigJs |
大型 JS 库延迟加载 | echarts、埋点 SDK 等 |
pagesOrder |
订单流程 | 订单相关页面 |
pagesService |
服务详情 | 课程/文章/视频详情 |
pagesHome |
个人中心子页面 | 设置、订单列表等 |
4.3 组件归属决策树
组件被谁使用?
├── 仅主包页面 → 放主包 components/
├── 仅单个分包 → 放该分包内
├── 多个分包 → 放主包 components/
└── 主包 + 分包 → 放主包 components/
五、分包预下载配置
对于分包后的高频页面,建议配置预下载提升体验:
json
// pages.json
{
"preloadRule": {
"pages/service/service": {
"network": "all",
"packages": ["pagesBigJs", "pagesOrder"]
},
"pages/home/home": {
"network": "all",
"packages": ["pagesBigJs", "pagesOrder"]
}
}
}
六、验证清单
分包优化完成后,需要验证:
6.1 构建验证
bash
# 小程序构建
npx uni build -p mp-weixin --mode hexin
# 检查主包体积
ls -lh dist/build/mp-weixin/common/vendor.js
# 检查分包体积
ls -lh dist/build/mp-weixin/pagesBigJs/
6.2 代码检查
bash
# 确认 echarts 不在主包
grep -r "registerPreprocessor" dist/build/mp-weixin/common/vendor.js
# 应该无输出
# 确认 echarts 在分包
grep -r "registerPreprocessor" dist/build/mp-weixin/pagesBigJs/
# 应该有输出
6.3 功能验证
- 主包 tabBar 页面正常加载
- 分包页面跳转正常
- 图表组件正常渲染(可能有轻微延迟)
- 埋点数据正常上报
七、总结
核心要点
- 分包引用限制:主包不能 require 分包资源,但页面跳转不受限
- 延迟加载模式 :通过
import()动态加载,编译为require.async(),将大型库移到分包 - 按需引入 :使用
echarts/core替代全量引入,结合 tree-shaking 减小体积 - 标签名匹配:小程序端组件标签名必须与 import 变量名完全一致
- 纯对象替代:渐变色等使用纯对象描述,避免依赖未加载的库实例
效果数据
| 优化项 | 节省体积 |
|---|---|
| 订单页面分包化 | ~200 KB |
| 埋点 SDK 延迟加载 | ~120 KB |
| ECharts 延迟加载 | ~360 KB |
| 总计 | ~680 KB |
主包体积从 2064 KB → 1512 KB ,余量从 -52 KB → +536 KB,彻底解决超标问题。
本文基于 uni-app + Vite 构建的微信小程序项目实践,适用于 uni-app Vue3 版本。