极致性能:UniApp 分包国际化异步加载方案
在开发大型小程序应用时,随着业务模块的增多,国际化(i18n)文件往往会变得异常庞大。如果将所有语言包都打包在主包中,不仅会挤占宝贵的主包体积(小程序限制 2MB),还会严重拖慢小程序的首屏启动速度。
本文将详细介绍本项目采用的 “分包异步加载” (Lazy-Load i18n) 方案,通过 路由监听 + 动态注入 的方式,实现了“访问哪个分包,就加载哪个分包的语言包”。
一、 传统方案的痛点
通常我们配置 vue-i18n 是这样的:
// main.ts
import zh from './locales/zh.json'; // 包含所有模块的翻译
import en from './locales/en.json';
const i18n = createI18n({
messages: { zh, en }
});
- 问题 1:主包体积爆炸。所有业务模块(商城、点餐、会员、分销...)的翻译都在
zh.json里,哪怕用户只访问首页,也必须下载完整的语言包。 - 问题 2:维护困难。所有模块的 key 混在一起,容易命名冲突,且文件行数动辄上千行。
二、 核心思路:按需加载,动态合并
我们的目标是:
- 主包只留基础:
src/i18n/下只放通用的翻译(如“确定”、“取消”、“加载中”)。 - 分包各自独立:每个分包目录下维护自己的
i18n文件夹(如src/subPackageScanQrOrder/i18n/)。 - 运行时注入:当用户跳转到某个分包页面时,自动读取并合并该分包的翻译内容到全局
i18n实例中。
三、 代码实现详解
1. 建立分包映射表
在 src/store/i18/index.ts 中,我们利用 Pinia Store 来管理加载逻辑。首先,需要定义分包路径与语言包文件的映射关系。
注意:这里使用了条件编译,分别处理小程序(
require)和 H5(import)的加载差异。
// i18n 模块映射表
const i18nMap: Record<string, () => Promise<any>> = {
// #ifdef MP
// 小程序环境使用 require
subPackageScanQrOrder: () => require('../../subPackageScanQrOrder/i18n/index'),
// #endif
// #ifndef MP
// H5 环境使用动态 import
// @ts-ignore
subPackageScanQrOrder: () => import('../../subPackageScanQrOrder/i18n/index')
// #endif
};
2. 实现动态合并逻辑
我们需要一个核心方法 mergeLocalMessageByRouter,它接收当前路由路径,识别出分包名,然后加载对应的语言文件。
const mergeLocalMessageByRouter = async (hashPath: string) => {
// 1. 提取分包前缀 (例如: subPackageScanQrOrder/pages/index -> subPackageScanQrOrder)
let pathPrefix = hashPath.split('/').filter(Boolean)[0];
if (!pathPrefix) return;
// 2. 查找映射加载器
const importer = i18nMap[pathPrefix];
if (importer) {
try {
// 3. 异步加载文件
const module: any = await importer();
const messages = module.default || module;
// 4. 合并到全局 i18n 实例
if (messages) {
await mergeToGlobal(pathPrefix, messages);
}
} catch (error) {
console.error(`[i18n] Failed to load messages: ${pathPrefix}`, error);
}
}
};
mergeToGlobal 方法负责调用 vue-i18n 的 API 进行合并,并增加了缓存机制防止重复加载:
const cache = new Map<string, Record<string, string>>();
const mergeToGlobal = async (namespace: string, messages: any) => {
const key = `${i18n.global.locale}_${namespace}`;
if (cache.has(key)) return; // 命中缓存,直接返回
// 调用 vue-i18n 的 mergeLocaleMessage API
Object.keys(messages).forEach(lang => {
i18n.global.mergeLocaleMessage(lang, messages[lang]);
});
cache.set(key, messages); // 写入缓存
};
3. 路由监听与触发
在应用的根组件 App.ku.vue 中,我们解析当前路由参数,并在初始化时触发加载。
<script setup lang="ts">
import { useI18nStore } from './store/i18';
import { parseRouteParams } from './utils/tools';
// 获取当前页面路径
const { hashPath } = parseRouteParams();
if (hashPath) {
// 触发分包语言包加载
useI18nStore().mergeLocalMessageByRouter(hashPath);
}
</script>
四、 扩展能力:支持远程动态下发
这套架构设计的另一个显著优势是高扩展性。当前的实现是从本地分包加载 JSON 文件,但只需微调 mergeLocalMessageByRouter 中的加载器逻辑,即可轻松升级为远程接口获取模式。
场景价值
- 实时更新:运营人员在后台修改文案后,用户无需更新小程序版本即可生效。
- 极致轻量:本地连 JSON 文件都不需要打包,进一步压缩包体积。
改造思路
只需将 i18nMap 中的 require/import 替换为 API 请求即可:
// 改造后的加载逻辑示意
const mergeLocalMessageByRouter = async (hashPath: string) => {
const pathPrefix = hashPath.split('/').filter(Boolean)[0];
// 优先尝试从接口获取最新翻译
try {
const remoteMessages = await fetchI18nFromApi(pathPrefix);
if (remoteMessages) {
await mergeToGlobal(pathPrefix, remoteMessages);
return;
}
} catch (e) {
// 接口失败降级为本地加载
const importer = i18nMap[pathPrefix];
// ... 原有本地加载逻辑
}
};
五、 方案成效
- 包体积优化:主包体积显著减小,只包含最基础的翻译。各业务模块的翻译随分包下载,不占用主包空间。
- 性能提升:减少了首屏初始化时需要加载和解析的 JSON 数据量。
- 开发体验:开发者只需关注当前分包下的
i18n目录,模块解耦,维护更清晰。 - 无感切换:对用户而言,进入分包页面时语言包已自动合并完成,体验流畅无感知。
- 未来可期:预留了远程下发接口,为未来的“云端文案配置”打下了架构基础。
通过这套方案,我们成功解决了大型小程序应用中国际化文件管理的痛点,实现了性能与可维护性的双赢。