极致性能:UniApp 分包国际化异步加载方案

43 阅读4分钟

极致性能: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 混在一起,容易命名冲突,且文件行数动辄上千行。

二、 核心思路:按需加载,动态合并

我们的目标是:

  1. 主包只留基础src/i18n/ 下只放通用的翻译(如“确定”、“取消”、“加载中”)。
  2. 分包各自独立:每个分包目录下维护自己的 i18n 文件夹(如 src/subPackageScanQrOrder/i18n/)。
  3. 运行时注入:当用户跳转到某个分包页面时,自动读取并合并该分包的翻译内容到全局 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];
    // ... 原有本地加载逻辑
  }
};

五、 方案成效

  1. 包体积优化:主包体积显著减小,只包含最基础的翻译。各业务模块的翻译随分包下载,不占用主包空间。
  2. 性能提升:减少了首屏初始化时需要加载和解析的 JSON 数据量。
  3. 开发体验:开发者只需关注当前分包下的 i18n 目录,模块解耦,维护更清晰。
  4. 无感切换:对用户而言,进入分包页面时语言包已自动合并完成,体验流畅无感知。
  5. 未来可期:预留了远程下发接口,为未来的“云端文案配置”打下了架构基础。

通过这套方案,我们成功解决了大型小程序应用中国际化文件管理的痛点,实现了性能与可维护性的双赢。