refactor(i18n): 优化多语言缓存逻辑,提升翻译性能
1. 新增多语言缓存体系,包括合并包缓存、字段解析结果缓存和页面路由缓存 2. 新增缓存失效方法,在语言更新、切换租户时统一清理缓存 3. 重构$lang方法,复用页面路由缓存避免重复计算 4. 增加字段结果缓存,命中直接返回避免重复解析 5. 复用合并后的语言包,减少深拷贝和合并操作次数 6. 清理冗余递归深度注释
This commit is contained in:
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user