refactor(i18n): 优化多语言缓存逻辑,提升翻译性能

1. 新增多语言缓存体系,包括合并包缓存、字段解析结果缓存和页面路由缓存
2. 新增缓存失效方法,在语言更新、切换租户时统一清理缓存
3. 重构$lang方法,复用页面路由缓存避免重复计算
4. 增加字段结果缓存,命中直接返回避免重复解析
5. 复用合并后的语言包,减少深拷贝和合并操作次数
6. 清理冗余递归深度注释
This commit is contained in:
2026-06-16 17:04:03 +08:00
parent efd7cdb8d7
commit 2e85e11e92

View File

@@ -6,6 +6,29 @@ import { Platform } from './platform.js';
// 缓存已加载的语言包
const loadedLangPacks = {};
// 缓存合并后的语言包(common + page + 远端覆写)
// key: `${locale}__${langPath}__${overrideVersion}, value: 合并后的对象
const _mergedLangCache = {};
let _overrideVersion = 0;
// 缓存字段解析结果
// key: `${locale}__${langPath}__${overrideVersion}__${field}, value: 翻译后的字符串
const _fieldResultCache = {};
// 缓存当前页面路由(页面生命周期内不变)
// key: 页面实例标识, value: langPath
let _cachedLangPath = '';
let _cachedLangPathPage = null;
// 清除所有语言缓存(切语言/切租户/远端覆写更新时调用)
function invalidateLangCaches() {
for (const k in _mergedLangCache) { if (Object.prototype.hasOwnProperty.call(_mergedLangCache, k)) delete _mergedLangCache[k]; }
for (const k in _fieldResultCache) { if (Object.prototype.hasOwnProperty.call(_fieldResultCache, k)) delete _fieldResultCache[k]; }
_cachedLangPath = '';
_cachedLangPathPage = null;
_overrideVersion++;
}
// 递归调用保护:防止 $lang() 被间接递归(如响应式 getter 回调中再次调用 $lang)
let _langRecursionDepth = 0;
const MAX_LANG_RECURSION_DEPTH = 8;
@@ -371,6 +394,7 @@ async function loadOverrides(lang) {
* 模板里的 {{$lang('xxx')}} 就会重新执行,读到最新 overrideCache
*/
function emitUpdated(lang) {
invalidateLangCaches();
try {
const store = require('@/store/index.js').default;
store.commit('setComponentRefresh');
@@ -493,76 +517,98 @@ export default {
}
_langRecursionDepth++;
try {
let _page = getCurrentPages()[getCurrentPages().length - 1];
const locale = getCurrentLocale();
let value = field;
let langPath = '';
try {
//公共语言包(同步加载)
var lang = loadLangPackSync(locale, 'common');
//当前页面语言包(同步加载)
// 关键修复:不依赖 _page 是否存在,页面未完全初始化时至少能使用 common 语言包
let route = '';
if (_page) {
route = _page.route || (_page.$page && _page.$page.path) || '';
}
// ------- 优化1: 页面路由缓存(页面生命周期内不变) -------
// 每次都检查当前页面实例是否变化(页面切换后需重新计算)
let _page = getCurrentPages()[getCurrentPages().length - 1];
let langPath;
if (_page && _page === _cachedLangPathPage && _cachedLangPath) {
// 同一页面实例,直接复用缓存
langPath = _cachedLangPath;
} else if (_page) {
// 新页面或首次计算,重新计算并缓存
const route = _page.route || (_page.$page && _page.$page.path) || '';
langPath = route ? processRoutePath(route) : '';
_cachedLangPath = langPath;
_cachedLangPathPage = _page;
} else {
// _page 为空时,只使用 common 语言包
langPath = '';
_cachedLangPath = '';
_cachedLangPathPage = null;
}
// 加载当前页面语言包,langPath 为空时返回空对象(只使用 common)
let currentPageLang = langPath ? loadLangPackSync(locale, langPath) : {};
// ------- 优化2: 字段解析结果缓存(命中直接返回) -------
const fieldCacheKey = `${locale}__${langPath}__${_overrideVersion}__${field}`;
const cachedFieldResult = _fieldResultCache[fieldCacheKey];
if (cachedFieldResult !== undefined) {
let value = cachedFieldResult;
// 带参数的翻译(如 hello {0})需要在缓存后仍做参数替换
if (arguments.length > 1) {
for (let i = 1; i < arguments.length; i++) {
value = String(value).replace("{" + (i - 1) + "}", arguments[i]);
}
}
return value;
}
// 合并语言包 + 远端 KV 覆写
// 关键修复:使用深复制代替 Object.assign,避免 applyOverrides 污染 loadedLangPacks 缓存
const mergedLang = Object.assign(deepCloneLang(lang), deepCloneLang(currentPageLang));
const ovr = overrideCache[getOverrideMemKey(locale)];
if (ovr && ovr.map) applyOverrides(mergedLang, ovr.map);
// ------- 优化3: 合并语言包缓存(避免每次 deepClone + merge + 覆写应用) -------
const mergeCacheKey = `${locale}__${langPath}__${_overrideVersion}`;
let mergedLang = _mergedLangCache[mergeCacheKey];
if (!mergedLang) {
try {
const lang = loadLangPackSync(locale, 'common');
const currentPageLang = langPath ? loadLangPackSync(locale, langPath) : {};
mergedLang = Object.assign(deepCloneLang(lang), 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;
}
// 解析字段
var arr = field.split(".");
if (arr.length > 1) {
// 处理嵌套属性,如 common.currencySymbol
// 增加 visited 检测:防止嵌套对象中的循环引用导致遍历异常
let temp = mergedLang;
let tempVisited = new Set([temp]);
let found = true;
for (let key of arr) {
if (temp[key] !== undefined) {
temp = temp[key];
if (tempVisited.has(temp)) {
// 检测到循环引用,立即中断,回退到原始 field
found = false;
break;
}
tempVisited.add(temp);
} else {
// ------- 字段解析 -------
let value = field;
var arr = field.split(".");
if (arr.length > 1) {
let temp = mergedLang;
let tempVisited = new Set([temp]);
let found = true;
for (let key of arr) {
if (temp[key] !== undefined) {
temp = temp[key];
if (tempVisited.has(temp)) {
found = false;
break;
}
tempVisited.add(temp);
} else {
found = false;
break;
}
// 关键修复:确保 value 始终是字符串,不是对象
value = found ? (typeof temp === 'string' ? temp : field) : field;
} else {
const raw = mergedLang[field];
// 关键修复:确保 value 始终是字符串,不是对象
value = (raw !== undefined && typeof raw === 'string') ? raw : field;
}
} catch (e) {
console.error('解析语言包失败:', e, { langPath, field, locale });
value = field;
value = found ? (typeof temp === 'string' ? temp : field) : field;
} else {
const raw = mergedLang[field];
value = (raw !== undefined && typeof raw === 'string') ? raw : field;
}
// 将结果存入字段缓存(不含参数替换部分:参数每次可能不同)
_fieldResultCache[fieldCacheKey] = value;
// 带参数的翻译
if (arguments.length > 1) {
//有参数,需要替换
for (var i = 1; i < arguments.length; i++) {
for (let i = 1; i < arguments.length; i++) {
value = String(value).replace("{" + (i - 1) + "}", arguments[i]);
}
}
if (value == undefined || (value == 'title' && field == 'title')) value = ''; // field
// 多语言调试,只在开发模式输出
if (value == undefined || (value == 'title' && field == 'title')) value = '';
// 多语言调试
const __DEV__ = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production');
if (__DEV__ && field == value) {
console.warn(`[i18n] missing key "${field}" in ${langPath || '<root>'} (${locale})`);
@@ -570,7 +616,6 @@ export default {
return value;
} finally {
// 确保无论正常返回还是异常退出,递归深度计数器都会递减
_langRecursionDepth--;
if (_langRecursionDepth < 0) _langRecursionDepth = 0;
}
@@ -591,6 +636,9 @@ export default {
invalidateStorageCache('lang');
const locale = getCurrentLocale();
// 切语言后清除语言包合并缓存 + 字段解析缓存
invalidateLangCaches();
// 清空已加载的"非新语种"语言包缓存
// 用 Object.keys 防止被原型链污染(for...in 会遍历继承属性)
Object.keys(loadedLangPacks).forEach(key => {