feat(i18n): 实现remote-only打包模式,优化首屏加载和产物体积

1. 新增remote-only模式下的空语言包stub
2. 新增缓存有效期配置并对齐后端Cache::set 600s
3. 重构语言包加载逻辑,适配remote-only模式
4. 添加webpack替换插件,排除本地lang目录减少产物体积
5. 优化Vue挂载时机,等待语言包就绪后再渲染首屏
6. 改进缓存策略,新增内存缓存TTL校验和语种缓存淘汰
7. 修复语言包缓存key生成逻辑,支持uniacid和appType隔离
This commit is contained in:
2026-06-30 14:04:52 +08:00
parent 55a5a68517
commit 50a1c1c695
7 changed files with 176 additions and 37 deletions

View File

@@ -575,8 +575,10 @@ function getOverrideStorageKey(lang) {
* 内存 cache key: <uniacid>_<appType>_<lang>
* 必须与 storage key 三维一致 — 切租户时内存不会"巧合"命中旧值
*/
function getOverrideMemKey(lang) {
return `${getCurrentUniacid()}_${getCurrentAppType()}_${lang}`;
function getOverrideMemKey(lang, uniacid, appType) {
const u = uniacid !== undefined ? uniacid : getCurrentUniacid();
const a = appType !== undefined ? appType : getCurrentAppType();
return `${u}_${a}_${lang}`;
}
/**
@@ -833,8 +835,19 @@ export default {
} catch (e) {}
try {
const { tree, version, source } = await loadBundleTree({ lang });
remoteTreeCache[getOverrideMemKey(lang)] = { version, tree, ts: Date.now(), source };
const uniacid = getCurrentUniacid();
const appType = getCurrentAppType();
let result = await loadBundleTree({ lang, appType, uniacid });
// 只要不是从远端获取的(memory/cache),都强制刷新一次,避免旧缓存结构导致 missing key
if (result.source !== 'remote') {
try {
result = await loadBundleTree({ lang, appType, uniacid, force: true });
} catch (e) {
// 网络刷新失败仍用缓存兜底
}
}
const { tree, version, source } = result;
remoteTreeCache[getOverrideMemKey(lang, uniacid, appType)] = { version, tree, ts: Date.now(), source };
setRemoteStatus('ok', { lang });
// bundle 就绪后清空字段缓存并触发全局刷新,避免首屏组件在 boot 前渲染的 raw key 被缓存
emitUpdated(lang);
@@ -929,7 +942,7 @@ export default {
getRemoteStatus() { return remoteStatus; },
/**
* 当前 source 配置(规范化后):'local' | 'remote' | 'hybrid'
* 当前 source 配置(规范化后):'local' | 'remote' | 'hybrid' | 'remote-only'
*/
getSource() { return SOURCE; },
@@ -1116,30 +1129,46 @@ export default {
invalidateStorageCache('lang');
const locale = getCurrentLocale();
// 切语言后清除语言包合并缓存 + 字段解析缓存
// 切语言后清除语言包合并缓存 + 字段解析缓存(这些是翻译结果缓存,必须清)
invalidateLangCaches();
// 清空已加载的"非新语种"语言包缓存
// 用 Object.keys 防止被原型链污染(for...in 会遍历继承属性)
// 策略优化: 保留旧语种的 bundle/override 缓存,只在内存中限制最大语种数
// 这样切回旧语种时能直接命中缓存,不重复请求
// 仅清空 loadedLangPacks 的合并结果缓存(非远端原始数据)
Object.keys(loadedLangPacks).forEach(key => {
if (!key.startsWith(locale + '_')) delete loadedLangPacks[key];
});
// 清空内存里的"非新语种"远端覆写 — 防止反复切语种导致内存泄漏
Object.keys(overrideCache).forEach(mk => {
if (!mk.endsWith('_' + locale)) delete overrideCache[mk];
});
// remote-only 模式:清除旧语种 bundle 缓存
Object.keys(remoteTreeCache).forEach(mk => {
if (!mk.endsWith('_' + locale)) delete remoteTreeCache[mk];
});
// 限制 remoteTreeCache 最大保留 3 个语种,超出时淘汰最旧的
const maxLocaleCache = 3;
const cachedLocales = Object.keys(remoteTreeCache);
if (cachedLocales.length >= maxLocaleCache) {
// 按时间戳排序,淘汰最旧的非当前语种
const sorted = cachedLocales
.filter(mk => !mk.endsWith('_' + locale))
.sort((a, b) => (remoteTreeCache[a].ts || 0) - (remoteTreeCache[b].ts || 0));
while (sorted.length >= maxLocaleCache) {
const evict = sorted.shift();
delete remoteTreeCache[evict];
}
}
// 切换语种后,先 await 远端覆写(有 storage 缓存的同语种秒开,无缓存的会触发一次网络)
// 远端失败/超时也返回 — await 拿到的就是 { ok:false, source:'local' },不会阻塞后续 refresh/reLaunch
// 注意:loadOverrides 内部已 emitUpdated(若成功),refresh() 内部不再重复 emit
if (IS_REMOTE_ONLY) {
try {
const { tree, version, source } = await loadBundleTree({ lang: value });
remoteTreeCache[getOverrideMemKey(value)] = { version, tree, ts: Date.now(), source };
const uniacid = getCurrentUniacid();
const appType = getCurrentAppType();
const { tree, version, source } = await loadBundleTree({ lang: value, uniacid, appType });
remoteTreeCache[getOverrideMemKey(value, uniacid, appType)] = { version, tree, ts: Date.now(), source };
setRemoteStatus('ok', { lang: value });
// 新语种 bundle 就绪后,同步 App 全局 promise 并触发刷新
try {
const app = getApp();
if (app && app.$langReady) app.$langReady = Promise.resolve({ ok: true, source, status: 'ok', version, lang: value });
} catch (e) {}
emitUpdated(value);
} catch (e) { /* 静默 */ }
} else if (SOURCE !== 'local') {
try { await loadOverrides(value); } catch (e) { /* 静默 */ }