refactor: 优化多语言结构并新增远端语言包加载能力

1. 扁平化多语言文件层级,移除嵌套的命名空间
2. 更新组件内多语言调用路径,适配新的语言包结构
3. 重构HTTP请求错误处理逻辑
4. 新增remote-only模式的远端语言包加载器
5. 新增多语言数据源remote-only配置项
This commit is contained in:
2026-06-30 10:03:25 +08:00
parent 32db276e68
commit e6d47a3d78
13 changed files with 477 additions and 117 deletions

View File

@@ -2,6 +2,7 @@ import { langConfig } from './config-external.js';
import Config from './config.js';
import http from './http.js';
import { Platform } from './platform.js';
import { loadBundleTree, getCachedBundle, clearBundleCache } from './lang-loader.js';
// 缓存已加载的语言包
const loadedLangPacks = {};
@@ -425,6 +426,10 @@ const overrideCache = {};
// 远端覆写:并发去重
const overrideInflight = {};
// remote-only 模式:缓存远端 bundle tree
// key: getOverrideMemKey(locale), value: { version, tree, ts, source }
const remoteTreeCache = {};
// 远端可用性三态机:'unknown'(启动后未拉过) | 'ok'(最近一次成功) | 'fail'(最近一次失败)
// 用 epoch ms 记录"最近一次状态变更时间",便于 UI 显示"离线 X 秒"
let remoteStatus = 'unknown';
@@ -432,12 +437,13 @@ let remoteStatusAt = 0;
const remoteStatusListeners = [];
/**
* 校验 source 配置:只接受 local / remote / hybrid,其它一律视为 remote(安全默认)
* 校验 source 配置:只接受 local / remote / hybrid / remote-only,其它一律视为 remote(安全默认)
*/
function normalizeSource(s) {
return ['local', 'remote', 'hybrid'].includes(s) ? s : 'remote';
return ['local', 'remote', 'hybrid', 'remote-only'].includes(s) ? s : 'remote';
}
const SOURCE = normalizeSource(langConfig.source);
const IS_REMOTE_ONLY = SOURCE === 'remote-only';
/**
* 远端失败原因归一(对外稳定枚举,内部细节不外泄)
@@ -733,6 +739,41 @@ function emitUpdated(lang) {
try { uni.$emit && uni.$emit('lang:updated', { lang }); } catch (e) {}
}
/**
* remote-only 模式下,为当前页面做 tree 归一化
* 把页面路径对应的 subtree 合并到顶层,兼容 $lang('title') 等短 key 调用
*/
function normalizeRemoteTreeForPage(tree, langPath) {
if (!tree || typeof tree !== 'object') return tree || {};
if (!langPath) return tree;
const segments = String(langPath).split('/').filter(Boolean);
if (!segments.length) return tree;
let leaf = tree;
let hasLeaf = true;
for (const segment of segments) {
const key = toCamelCaseName(segment);
if (leaf && typeof leaf === 'object' && !Array.isArray(leaf) && leaf[key] !== undefined) {
leaf = leaf[key];
} else {
hasLeaf = false;
break;
}
}
const lastSegment = toCamelCaseName(segments[segments.length - 1]);
const currentPageLeaf =
hasLeaf && leaf && typeof leaf === 'object' && !Array.isArray(leaf)
? leaf
: (tree[lastSegment] && typeof tree[lastSegment] === 'object' && !Array.isArray(tree[lastSegment]) ? tree[lastSegment] : null);
if (!currentPageLeaf) return tree;
// 页面 leaf 合并到顶层,同时也保留完整路径
return deepMergeLang(deepCloneLang(tree), deepCloneLang(currentPageLeaf));
}
// 导出主体
export default {
langList: langConfig.langList,
@@ -780,6 +821,36 @@ export default {
return { ok: true, source: 'local', status: remoteStatus, lang };
}
// remote-only 模式:加载完整 bundle tree
if (IS_REMOTE_ONLY) {
const currentUid = getCurrentUniacid();
try {
const lastUid = uni.getStorageSync('lang_last_uniacid');
if (lastUid && String(lastUid) !== String(currentUid)) {
clearBundleCache({ lang });
}
uni.setStorageSync('lang_last_uniacid', currentUid);
} catch (e) {}
try {
const { tree, version, source } = await loadBundleTree({ lang });
remoteTreeCache[getOverrideMemKey(lang)] = { version, tree, ts: Date.now(), source };
setRemoteStatus('ok', { lang });
// bundle 就绪后清空字段缓存并触发全局刷新,避免首屏组件在 boot 前渲染的 raw key 被缓存
emitUpdated(lang);
return { ok: true, source, status: 'ok', version, lang };
} catch (e) {
const cached = getCachedBundle({ lang });
if (cached) {
remoteTreeCache[getOverrideMemKey(lang)] = { version: cached.version, tree: cached.tree, ts: cached.ts, source: 'cache' };
setRemoteStatus('fail', { reason: 'exception', lang });
return { ok: false, source: 'cache', status: 'fail', reason: e.message || 'bundle failed', lang };
}
setRemoteStatus('fail', { reason: 'exception', lang });
return { ok: false, source: 'local', status: 'fail', reason: e.message || 'bundle failed', lang };
}
}
const remote = langConfig.remote;
const t = new Promise((r) => setTimeout(() => r({ __timeout: true, code: -1, message: 'timeout' }), remote.bootTimeout));
@@ -822,6 +893,14 @@ export default {
// 切租户后清除 storage 缓存,确保下次读时拿到新 uniacid
invalidateStorageCache('uniacid');
const lang = getCurrentLocale();
if (IS_REMOTE_ONLY) {
clearBundleCache({ lang });
delete remoteTreeCache[getOverrideMemKey(lang)];
loadBundleTree({ lang });
return;
}
const sk = getOverrideStorageKey(lang);
try { uni.removeStorageSync(sk); } catch (e) {}
delete overrideCache[getOverrideMemKey(lang)];
@@ -920,10 +999,16 @@ export default {
}
// ------- 优化3: 合并语言包缓存(避免每次 deepClone + merge + 覆写应用) -------
const mergeCacheKey = `${locale}__${langPath}__${_overrideVersion}`;
let mergedLang = _mergedLangCache[mergeCacheKey];
if (!mergedLang) {
try {
const mergeCacheKey = `${locale}__${langPath}__${_overrideVersion}`;
let mergedLang = _mergedLangCache[mergeCacheKey];
if (!mergedLang) {
try {
if (IS_REMOTE_ONLY) {
// remote-only 模式:直接复用远端 bundle tree,不再 require 本地文件
const cache = remoteTreeCache[getOverrideMemKey(locale)];
const baseTree = (cache && cache.tree) ? cache.tree : {};
mergedLang = normalizeRemoteTreeForPage(baseTree, langPath);
} else {
const lang = loadLangPackSync(locale, 'common');
const currentPageLang = langPath ? normalizePageLangPack(langPath, loadLangPackSync(locale, langPath)) : {};
mergedLang = deepCloneLang(lang);
@@ -936,12 +1021,13 @@ export default {
mergedLang = deepMergeLang(mergedLang, deepCloneLang(currentPageLang));
const ovr = overrideCache[getOverrideMemKey(locale)];
if (ovr && ovr.map) applyOverrides(mergedLang, ovr.map);
} catch (e) {
console.error('解析语言包失败:', e, { langPath, field, locale });
mergedLang = {};
}
_mergedLangCache[mergeCacheKey] = mergedLang;
} catch (e) {
console.error('解析语言包失败:', e, { langPath, field, locale });
mergedLang = {};
}
_mergedLangCache[mergeCacheKey] = mergedLang;
}
// ------- 字段解析 -------
let value = field;
@@ -1042,11 +1128,20 @@ export default {
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];
});
// 切换语种后,先 await 远端覆写(有 storage 缓存的同语种秒开,无缓存的会触发一次网络)
// 远端失败/超时也返回 — await 拿到的就是 { ok:false, source:'local' },不会阻塞后续 refresh/reLaunch
// 注意:loadOverrides 内部已 emitUpdated(若成功),refresh() 内部不再重复 emit
if (SOURCE !== 'local') {
if (IS_REMOTE_ONLY) {
try {
const { tree, version, source } = await loadBundleTree({ lang: value });
remoteTreeCache[getOverrideMemKey(value)] = { version, tree, ts: Date.now(), source };
} catch (e) { /* 静默 */ }
} else if (SOURCE !== 'local') {
try { await loadOverrides(value); } catch (e) { /* 静默 */ }
}
@@ -1119,6 +1214,16 @@ export default {
const getCommonName = (langType) => {
try {
if (tmp[langType]) return tmp[langType];
// remote-only 模式下优先从 bundle tree 读取
if (IS_REMOTE_ONLY) {
const cache = remoteTreeCache[getOverrideMemKey(langType)];
if (cache && cache.tree && cache.tree.common && cache.tree.common.name) {
tmp[langType] = cache.tree.common.name;
return tmp[langType];
}
}
const data = require(`@/lang/${langType}/common.js`).lang;
tmp[langType] = (data && data.common) ? data.common.name : langType;
return tmp[langType];