🚀 手把手教你实现 React Native 远程多语言热更新(保姆级教程)
你是否遇到过“仅仅因为改了一个错别字,就要重新打包、提交审核、等待上架”的崩溃时刻?😭
这篇文章将手把手教你搭建一套 “文案热更新系统”。 不用懂原理,跟着做,复制粘贴代码,你也能拥有“改完后台,App 立刻生效”的魔法!✨
📚 目录
第一步:安装依赖
打开你的终端,进入项目目录,运行以下命令。我们需要安装 i18next(多语言核心)和 react-native-mmkv(超快的本地存储)。
yarn add i18next react-i18next i18next-chained-backend i18next-http-backend react-native-mmkv react-native-localize
(如果你的项目还没用 yarn,用 npm install 也可以)
🚑 呼叫后端:你需要这两个接口
在开始写代码前,请把这段发给你的后端同事,让他帮你提供 2 个简单的接口。
1. 获取版本号接口
- 作用:App 启动时会调用它,问问服务器“现在最新的翻译是第几版?”
- 地址示例:
GET /api/language/versions - 返回格式:
[ { "cultureName": "zh-Hans", "version": "2" }, { "cultureName": "en", "version": "1" } ]
2. 获取翻译包接口
- 作用:如果 App 发现有新版本,就会调用这个接口下载具体的 JSON。
- 地址示例:
GET /api/language/json?lang=zh-Hans - 返回格式:
{ "common": { "title": "欢迎", "submit": "提交" } }
提示:如果没有后端支持,你也可以把这些 JSON 文件放在 OSS / S3 / GitHub Pages 上,直接通过 URL 访问静态文件也是一样的!
第二步:复制核心文件
请在你的 src 目录下创建一个 i18n 文件夹和一个 context 文件夹。
结构如下:
src/
├── i18n/
│ ├── MMKVBackend.js <-- 待会新建
│ └── index.js <-- 待会新建
└── context/
└── RemoteI18nContext.js <-- 待会新建
1. 创建缓存后端 (MMKVBackend.js)
作用:告诉 i18n,“请把下载好的翻译存在 MMKV 里,下次直接读,别老去下载”。
👉 直接复制以下代码到 src/i18n/MMKVBackend.js:
import { MMKV } from 'react-native-mmkv';
// 创建一个专门存翻译的库
const storage = new MMKV({ id: 'i18n-storage' });
class MMKVBackend {
static type = 'backend';
constructor(services, options = {}) {
this.init(services, options);
}
init(services, options = {}) {
this.services = services;
this.options = {
prefix: 'i18n_',
expirationTime: 30 * 24 * 60 * 60 * 1000, // 默认缓存30天
defaultVersion: '1',
versions: {},
...options,
};
}
read(language, namespace, callback) {
const key = `${this.options.prefix}${language}_${namespace}`;
try {
const data = storage.getString(key);
if (!data) return callback(null, null); // 没缓存,去找下一个后端
const { value, version } = JSON.parse(data);
// 这里可以加版本校验逻辑,简单起见先直接返回
console.log(`[i18n] Cache hit: ${language}/${namespace}`);
return callback(null, value);
} catch (err) {
return callback(null, null);
}
}
// 保存翻译到本地
save(language, namespace, data) {
const key = `${this.options.prefix}${language}_${namespace}`;
try {
const payload = {
value: data,
timestamp: Date.now(),
version: this.options.versions?.[language] || '1',
};
storage.set(key, JSON.stringify(payload));
console.log(`[i18n] Saved: ${language}/${namespace}`);
} catch (err) {}
}
}
export default MMKVBackend;
2. 初始化 i18n (index.js)
作用:启动 i18n,配置好“先读缓存,读不到再去网络”的策略。
👉 直接复制以下代码到 src/i18n/index.js:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import ChainedBackend from 'i18next-chained-backend';
import HttpBackend from 'i18next-http-backend';
import MMKVBackend from './MMKVBackend';
import * as RNLocalize from 'react-native-localize';
// 你的默认本地翻译文件(兜底用,万一没网也没缓存)
const enCommon = { common: { title: "Welcome" } };
const zhCommon = { common: { title: "欢迎" } };
i18n
.use(ChainedBackend) // 关键:链式后端
.use(initReactI18next)
.init({
compatibilityJSON: 'v4',
// 本地兜底资源
resources: {
en: enCommon,
'zh-Hans': zhCommon,
},
lng: RNLocalize.getLocales()[0]?.languageCode || 'en',
fallbackLng: 'en',
ns: ['common'],
defaultNS: 'common',
backend: {
// 顺序很重要:先找 MMKV,找不到再找 HTTP
backends: [MMKVBackend, HttpBackend],
backendOptions: [
{ prefix: 'i18n_' }, // MMKV 配置
{ loadPath: 'https://你的服务器地址/api/language/{{lng}}/{{ns}}' } // HTTP 配置 (其实主要靠Context手动下载)
]
},
});
export default i18n;
3. 创建热更新逻辑 (RemoteI18nContext.js)
作用:这是核心大脑。它负责在后台悄悄检查版本、下载新翻译、更新缓存、刷新界面。
👉 复制并修改以下代码到 src/context/RemoteI18nContext.js:
⚠️ 注意:代码中标记
// TODO:的地方需要换成你自己的 API 请求函数。
import React, { createContext, useContext, useEffect } from 'react';
import { MMKV } from 'react-native-mmkv';
import i18n from '../i18n'; // 引入刚才写的 i18n
// 专门存翻译的库(跟 MMKVBackend 保持一致)
const i18nStorage = new MMKV({ id: 'i18n-storage' });
const appStorage = new MMKV(); // 存版本号用
const RemoteI18nContext = createContext(null);
export const RemoteI18nProvider = ({ children, isSignedIn }) => {
// 1. App 启动时:马上加载本地缓存,保证不白屏
useEffect(() => {
loadFromCache();
}, []);
// 2. 登录后:静默检查更新
useEffect(() => {
if (isSignedIn) {
syncTranslations();
}
}, [isSignedIn]);
// 从缓存加载(保证离线可用)
const loadFromCache = () => {
const keys = i18nStorage.getAllKeys();
keys.forEach(key => {
if (!key.startsWith('i18n_')) return;
// key 格式: i18n_zh-Hans_common
const parts = key.split('_');
const lang = parts[1]; // zh-Hans
const ns = parts[2]; // common
const data = i18nStorage.getString(key);
if (data) {
const { value } = JSON.parse(data);
// 注入到内存
i18n.addResourceBundle(lang, ns, value, true, true);
}
});
};
// 检查服务端是否有新版本
const syncTranslations = async () => {
try {
// TODO: 替换成你的 API,获取服务端最新版本列表
// 假设返回结构: [{ cultureName: 'zh-Hans', version: '2' }, ...]
const serverVersions = await mockApiGetVersions();
// 读取本地保存的版本
const localVersionsStr = appStorage.getString('LANGUAGE_VERSIONS');
const localVersions = localVersionsStr ? JSON.parse(localVersionsStr) : {};
for (const item of serverVersions) {
const { cultureName, version } = item;
const localV = localVersions[cultureName] || '0';
// 如果服务器版本 > 本地版本
if (Number(version) > Number(localV)) {
console.log(`发现新版本 ${cultureName}: ${version}`);
await fetchAndApply(cultureName, version);
// 更新本地版本记录
localVersions[cultureName] = version;
}
}
appStorage.set('LANGUAGE_VERSIONS', JSON.stringify(localVersions));
} catch (e) {
console.error('同步失败', e);
}
};
// 下载并应用新翻译
const fetchAndApply = async (lang, version) => {
try {
// TODO: 替换成你的 API,下载具体的 JSON 文件
const data = await mockApiGetJson(lang);
// 1. 更新 i18n 内存
i18n.addResourceBundle(lang, 'common', data, true, true);
// 2. 存入 MMKV 供下次启动用
const cacheKey = `i18n_${lang}_common`;
i18nStorage.set(cacheKey, JSON.stringify({
value: data,
version: version,
timestamp: Date.now()
}));
// 3. 强制刷新当前界面
if (i18n.language === lang) {
i18n.changeLanguage(lang);
}
console.log(`语言包 ${lang} 更新成功!`);
} catch (e) {
console.error('下载失败', e);
}
};
// --- Mock API (实际使用请删除) ---
const mockApiGetVersions = async () => [
{ cultureName: 'zh-Hans', version: '2' }, // 模拟服务器说中文包是 v2
{ cultureName: 'en', version: '1' }
];
const mockApiGetJson = async (lang) => {
if (lang === 'zh-Hans') return { common: { title: "欢迎(这是热更新后的文案!)" } };
return { common: { title: "Welcome (Updated)" } };
};
// ------------------------------
return (
<RemoteI18nContext.Provider value={{ syncTranslations }}>
{children}
</RemoteI18nContext.Provider>
);
};
第三步:接入到 App
找到你的 App.js(入口文件),把 RemoteI18nProvider 包裹在最外层。
import React from 'react';
import { RemoteI18nProvider } from './src/context/RemoteI18nContext';
import Navigation from './src/navigation'; // 你的导航组件
const App = () => {
// 假设你有一个状态判断用户是否登录
const isSignedIn = true;
return (
<RemoteI18nProvider isSignedIn={isSignedIn}>
<Navigation />
</RemoteI18nProvider>
);
};
export default App;
第四步:在页面中使用
完全不用改习惯! 就像平时用 i18n 一样写代码。
import React from 'react';
import { Text, View } from 'react-native';
import { useTranslation } from 'react-i18next';
const HomeScreen = () => {
const { t } = useTranslation();
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
{/* 这里的文案会自动变成热更新后的版本 */}
<Text style={{ fontSize: 20 }}>{t('common.title')}</Text>
</View>
);
};
export default HomeScreen;
第五步:如何切换语言
在设置页或登录页,你可能需要让用户手动切换语言。非常简单,直接调用 i18n.changeLanguage 即可。
import React from 'react';
import { Button, View } from 'react-native';
import { useTranslation } from 'react-i18next';
import i18n from '../i18n'; // 引入我们初始化的 i18n 实例
const SettingsScreen = () => {
const { t } = useTranslation();
const changeToChinese = () => {
i18n.changeLanguage('zh-Hans'); // 切换到中文
// 提示:切换后,RemoteI18nContext 会自动去 MMKV 找有没有下载好的中文包,有就用,没有就用本地兜底
};
const changeToEnglish = () => {
i18n.changeLanguage('en'); // 切换到英文
};
return (
<View>
<Button title="中文" onPress={changeToChinese} />
<Button title="English" onPress={changeToEnglish} />
<Text>{t('common.title')}</Text>
</View>
);
};
小白原理课:它到底怎么运行的?
想象一下,你的 App 是一个餐厅,文案就是菜单。
-
以前的做法(发版): 菜单印在桌子上(写死在包里)。如果要改菜名,必须把所有桌子扔了,重新买桌子(重新发版用户下载)。
-
现在的做法(热更新):
- MMKV (本地缓存):我们在每张桌子上放了一张手写菜单。
- App 启动 (loadFromCache):客人坐下,直接看手写菜单点菜(秒开,不用等)。
- 静默同步 (syncTranslations):服务员(后台线程)悄悄去厨房问大厨(服务器):“今天菜单有变吗?”
- 热生效:大厨说:“‘宫保鸡丁’改名叫‘辣子鸡’了”。服务员立马把桌子上的手写菜单改了,客人下一眼看到的就是新菜名。
-
首次安装(无缓存):
- 客人第一次来餐厅,桌子上还没手写菜单。没关系,我们有印刷菜单(本地兜底
common.json)。客人先看印刷菜单点菜,等服务员从厨房拿来最新的手写菜单,下次来就能用新的了。
- 客人第一次来餐厅,桌子上还没手写菜单。没关系,我们有印刷菜单(本地兜底
总结:用户感觉不到任何下载过程,但你随时可以在后台改文案,所有人的 App 几秒钟后自动变身!🚀