Skip to content

UniApp 分包设计实践

这篇文章记录了把 FF14Search 的配装器、市价模块等大型功能拆进分包的过程,以及踩过的几个坑。


为什么需要分包

微信小程序对主包有 2MB 的体积限制,总包(主包 + 所有分包)不超过 20MB。FF14Search 光是游戏基础数据文件就有几百 KB,主包如果再塞进配装器的计算逻辑和静态数据,基本已经超限。

UniApp 的分包(subPackages)机制让我们可以把不常用或体量大的功能延迟加载——用户首次进入该分包页面时才触发下载,不会在启动时一次性加载全部代码。


分包结构设计

现有的分包按功能领域划分,每个分包尽量自给自足:

src/subPackages/
├── master/          # 道具详情(Garland 详细数据)
├── universalis/     # 市价详情(Universalis API + 独立 Store)
├── gearing/         # 配装器(计算逻辑、数据、Store、4 个页面)
└── utilities/       # 副本招募等小工具

划分原则:

  1. 按功能聚合,而非按页面数量——每个分包内相关的 API、Store、组件都放在一起
  2. 共享代码留主包,分包之间禁止互相引用
  3. 分包内允许有独立 Store,不强制把所有 Pinia Store 集中到 src/stores/

分包内的 Pinia Store

分包内使用 Pinia 有一个常见误区:以为 src/stores/ 里的 Store 才算"全局的",分包内定义的 Store 是"临时的"。

实际上 Pinia 的 Store 是单例的——无论在哪里调用 defineStore,只要 id 相同,拿到的就是同一个实例。分包内的 Store 只要在分包页面里 import 就会自动注册,和放在主包没有区别。

ts
// src/subPackages/universalis/stores/user-preference.ts
import { defineStore } from 'pinia'

export const useUserPreferenceStore = defineStore('universalis-user-pref', () => {
  const defaultServer = ref('紫水栈桥')

  function setDefaultServer(server: string) {
    defaultServer.value = server
    uni.setStorageSync('universalis_default_server', server)
  }

  return { defaultServer, setDefaultServer }
})

注意:分包 Store 的 id 建议加上分包前缀(如 universalis-user-pref),避免与主包或其他分包的 Store 命名冲突。


配装器的分包移植

配装器(subPackages/gearing)是最复杂的分包,来自开源配装工具的移植,包含:

  • 完整的属性计算引擎(暴击、直击、意志、速度等各属性的公式换算)
  • 所有职业的装备槽定义与限制规则
  • 全版本装备数据(JSON,几百 KB)
  • 魔晶石镶嵌收益计算(含超出上限后的溢出)
  • ShareCode 编解码(Base62 BigInt,v5 格式)

移植的主要工作是把原版逻辑改写为 Vue 3 Composition API,同时适配 UniApp 的运行环境(无 DOM、无 window 对象等)。

装备数据的加载策略

装备数据体积较大,不适合随代码打包。最终方案是把数据文件放在分包的 data/ 目录下,在用户首次进入配装器页面时动态 import

ts
// 懒加载配装数据,只在进入配装页面时执行
const { items } = await import('../data/items.json')

这样数据只随分包下载,不影响主包启动速度。


分包路由跳转

从主包跳转到分包页面,路径需要带上分包根目录:

ts
// 跳转到配装器(gearing 分包)
uni.navigateTo({
  url: '/subPackages/gearing/pages/gearset-list/index'
})

// 带参数跳转到市价详情(universalis 分包)
uni.navigateTo({
  url: `/subPackages/universalis/pages/market-price-detail/market-price-detail?itemId=${itemId}`
})

分包页面跳回主包则直接用主包路径,没有限制。


几个坑

1. 分包之间不能互相引用

如果 gearing 分包里的代码 importuniversalis 分包的模块,小程序会在运行时报路径找不到的错误。共享工具函数必须放在 src/utils/(主包)。

2. 条件编译要小心

UniApp 的条件编译(#ifdef MP-WEIXIN)在 .vue 文件的 <template><script> 块里都能工作,但在 .ts 文件里只在经过 Vite 处理的情况下生效。某些工具链版本会让条件编译在分包的 .ts 文件里失效,踩到的话可以改用运行时判断 uni.getSystemInfoSync().platform

3. 分包 Store 的初始化时机

分包 Store 在分包代码被加载前不会执行,所以如果主包里有逻辑依赖分包 Store 的数据,会拿到 undefined。解决方法是把共享状态留在主包 Store 里,分包 Store 只管分包自己用的偏好设置。


小结

UniApp 分包本质上是一种按需加载机制,设计时只需要想清楚两件事:

  1. 哪些功能是核心路径(启动就要),哪些是按需访问的(可以延迟加载)
  2. 分包内的代码如何与主包共享必要的工具和状态,同时不造成循环依赖

把这两点想清楚,分包带来的麻烦就不多,收益(包体积控制、启动速度)相当明显。