refactor(i18n): 完善remote-only模式下的首屏渲染和请求门控
1. 补充getCachedBundle的注释说明TTL过期缓存返回null 2. 修复globalConfig中onShow的重复判断逻辑,增加boot完成检查 3. 给http请求增加remote-only模式下的boot门控,等待国际化就绪后再发送业务请求 4. 重构main.js的平台差异化处理逻辑,统一各端初始化流程 5. 优化App.vue的boot流程,为小程序/鸿蒙增加加载遮罩 6. 新增bootCompleted状态标志和并发去重机制,完善lang.js的remote-only适配逻辑 7. 补充lang()方法在boot未完成时返回空串的逻辑,避免首屏raw key
This commit is contained in:
@@ -36,6 +36,14 @@ const _callUtil = (vm, method, ...args) => {
|
||||
export default {
|
||||
onLoad() { },
|
||||
onShow() {
|
||||
// remote-only 小程序/鸿蒙: boot 未完成时跳过 refresh,避免空翻译写入标题等
|
||||
// H5 不受影响(main.js 已 await boot 后才 $mount)
|
||||
if (this.$langConfig.isRemoteOnly && this.$langConfig.isRemoteOnly()) {
|
||||
if (!this.$langConfig.isBootCompleted || !this.$langConfig.isBootCompleted()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新多语言(仅刷新标题,不触发全页面 $forceUpdate)
|
||||
// 全量刷新仅在语言包真正变化时由 lang.js emitUpdated 触发
|
||||
this.$langConfig.refresh(false);
|
||||
|
||||
@@ -25,6 +25,35 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// remote-only boot 门控: 所有业务请求必须等国际化数据就绪后才发送
|
||||
// 这样业务请求返回时 lang() 已有翻译数据,不会造成 UI 污染
|
||||
// bundle 请求走 lang-loader.js 的 uni.request 直接发送,不受此门控影响
|
||||
const _isRemoteOnly = getLang().isRemoteOnly && getLang().isRemoteOnly();
|
||||
const _bootDone = getLang().isBootCompleted && getLang().isBootCompleted();
|
||||
if (_isRemoteOnly && !_bootDone) {
|
||||
// 获取 boot promise(优先从 App 实例拿,避免重复 boot)
|
||||
let bootPromise = null;
|
||||
try {
|
||||
const app = getApp();
|
||||
if (app && app.$langReady) bootPromise = app.$langReady;
|
||||
} catch (e) {}
|
||||
if (!bootPromise) bootPromise = getLang().boot();
|
||||
|
||||
if (params.async === false) {
|
||||
// 同步 Promise 模式: 链式等待 boot 后再发请求
|
||||
return bootPromise.then(() => this._sendRequestNow(params));
|
||||
} else {
|
||||
// 回调模式: boot 完成后再执行请求
|
||||
bootPromise.then(() => this._sendRequestNow(params));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return this._sendRequestNow(params);
|
||||
},
|
||||
|
||||
_sendRequestNow(params) {
|
||||
|
||||
var method = params.data != undefined ? 'POST' : 'GET',
|
||||
url = (Config.baseUrl + params.url).replace(/(?<!:)\/+/g, '/'),
|
||||
data = {
|
||||
|
||||
@@ -256,6 +256,7 @@ export async function loadBundleTree(options = {}) {
|
||||
|
||||
/**
|
||||
* 获取已缓存的 bundle(不触发网络请求)
|
||||
* 注意: 受 TTL 限制,过期缓存返回 null
|
||||
*/
|
||||
export function getCachedBundle(options = {}) {
|
||||
const lang = options.lang || getCurrentLocale();
|
||||
|
||||
@@ -430,6 +430,14 @@ const overrideInflight = {};
|
||||
// key: getOverrideMemKey(locale), value: { version, tree, ts, source }
|
||||
const remoteTreeCache = {};
|
||||
|
||||
// remote-only 模式: boot() 是否已完成(远端 bundle 已就绪)
|
||||
// boot 完成前, lang() 返回空串且不报 missing key, 避免首屏 raw key 和噪音警告
|
||||
// boot 完成后 emitUpdated 触发 $forceUpdate, 页面重新渲染显示真实翻译
|
||||
let bootCompleted = false;
|
||||
|
||||
// boot() 并发去重: 避免多次调用 boot 时重复执行逻辑和 emitUpdated
|
||||
let _bootInflight = null;
|
||||
|
||||
// 远端可用性三态机:'unknown'(启动后未拉过) | 'ok'(最近一次成功) | 'fail'(最近一次失败)
|
||||
// 用 epoch ms 记录"最近一次状态变更时间",便于 UI 显示"离线 X 秒"
|
||||
let remoteStatus = 'unknown';
|
||||
@@ -779,6 +787,20 @@ function normalizeRemoteTreeForPage(tree, langPath) {
|
||||
export default {
|
||||
langList: langConfig.langList,
|
||||
|
||||
/**
|
||||
* 是否 remote-only 模式
|
||||
*/
|
||||
isRemoteOnly() {
|
||||
return IS_REMOTE_ONLY;
|
||||
},
|
||||
|
||||
/**
|
||||
* boot() 是否已完成(remote-only 模式下用于判断翻译是否就绪)
|
||||
*/
|
||||
isBootCompleted() {
|
||||
return bootCompleted;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获得当前本地语言
|
||||
* @returns
|
||||
@@ -814,11 +836,22 @@ export default {
|
||||
*
|
||||
* @returns {Promise<{ok:boolean, source:'local'|'remote'|'cache'|'timeout', status:'unknown'|'ok'|'fail', reason?:string, lang:string}>}
|
||||
*/
|
||||
async boot() {
|
||||
boot() {
|
||||
// 并发去重: 同一次 boot 未完成时,后续调用复用同一个 promise
|
||||
if (_bootInflight) return _bootInflight;
|
||||
_bootInflight = this._bootImpl();
|
||||
const p = _bootInflight;
|
||||
// 无论成功失败,清理 inflight 允许后续重新 boot
|
||||
_bootInflight.then(() => { _bootInflight = null; }, () => { _bootInflight = null; });
|
||||
return p;
|
||||
},
|
||||
|
||||
async _bootImpl() {
|
||||
const lang = getCurrentLocale();
|
||||
|
||||
// source === 'local' 时不需要预热远端,本地全量已可用
|
||||
if (SOURCE === 'local') {
|
||||
bootCompleted = true;
|
||||
return { ok: true, source: 'local', status: remoteStatus, lang };
|
||||
}
|
||||
|
||||
@@ -833,9 +866,9 @@ export default {
|
||||
uni.setStorageSync('lang_last_uniacid', currentUid);
|
||||
} catch (e) {}
|
||||
|
||||
const uniacid = getCurrentUniacid();
|
||||
const appType = getCurrentAppType();
|
||||
try {
|
||||
const uniacid = getCurrentUniacid();
|
||||
const appType = getCurrentAppType();
|
||||
let result = await loadBundleTree({ lang, appType, uniacid });
|
||||
// 只要不是从远端获取的(memory/cache),都强制刷新一次,避免旧缓存结构导致 missing key
|
||||
if (result.source !== 'remote') {
|
||||
@@ -848,25 +881,29 @@ export default {
|
||||
const { tree, version, source } = result;
|
||||
remoteTreeCache[getOverrideMemKey(lang, uniacid, appType)] = { version, tree, ts: Date.now(), source };
|
||||
setRemoteStatus('ok', { lang });
|
||||
// bundle 就绪后清空字段缓存并触发全局刷新,避免首屏组件在 boot 前渲染的 raw key 被缓存
|
||||
bootCompleted = true;
|
||||
emitUpdated(lang);
|
||||
return { ok: true, source, status: 'ok', version, lang };
|
||||
} catch (e) {
|
||||
const cached = getCachedBundle({ lang });
|
||||
const cached = getCachedBundle({ lang, uniacid, appType });
|
||||
if (cached) {
|
||||
remoteTreeCache[getOverrideMemKey(lang)] = { version: cached.version, tree: cached.tree, ts: cached.ts, source: 'cache' };
|
||||
remoteTreeCache[getOverrideMemKey(lang, uniacid, appType)] = { version: cached.version, tree: cached.tree, ts: cached.ts, source: 'cache' };
|
||||
setRemoteStatus('fail', { reason: 'exception', lang });
|
||||
bootCompleted = true;
|
||||
emitUpdated(lang);
|
||||
return { ok: false, source: 'cache', status: 'fail', reason: e.message || 'bundle failed', lang };
|
||||
}
|
||||
setRemoteStatus('fail', { reason: 'exception', lang });
|
||||
bootCompleted = true;
|
||||
emitUpdated(lang);
|
||||
return { ok: false, source: 'local', status: 'fail', reason: e.message || 'bundle failed', lang };
|
||||
}
|
||||
}
|
||||
|
||||
// 非 remote-only: 走原有的 loadOverrides 逻辑
|
||||
const remote = langConfig.remote;
|
||||
const t = new Promise((r) => setTimeout(() => r({ __timeout: true, code: -1, message: 'timeout' }), remote.bootTimeout));
|
||||
|
||||
// 检测 uniacid 是否变了(防止 App.onLaunch 改了 uniacid 但没触发 onUniacidChange)
|
||||
const currentUid = getCurrentUniacid();
|
||||
try {
|
||||
const lastUid = uni.getStorageSync('lang_last_uniacid');
|
||||
@@ -876,13 +913,11 @@ export default {
|
||||
uni.setStorageSync('lang_last_uniacid', currentUid);
|
||||
} catch (e) {}
|
||||
|
||||
// 跑一次远端拉取(被超时切走时,fail 状态会由 loadOverrides 内部设置)
|
||||
const result = await Promise.race([loadOverrides(lang), t]);
|
||||
const finalStatus = remoteStatus;
|
||||
bootCompleted = true;
|
||||
|
||||
// 远端成功/失败与否,本地兜底都已可用
|
||||
if (result && result.__timeout) {
|
||||
// 超时分支:loadOverrides 内部 setRemoteStatus('fail') 已触发
|
||||
return { ok: false, source: 'timeout', status: finalStatus, reason: 'timeout', lang };
|
||||
}
|
||||
return {
|
||||
@@ -954,6 +989,14 @@ export default {
|
||||
if (_langRecursionDepth >= MAX_LANG_RECURSION_DEPTH) {
|
||||
return typeof field === 'string' ? field : String(field || '');
|
||||
}
|
||||
|
||||
// remote-only 模式(小程序/鸿蒙): boot 未完成时返回空串
|
||||
// H5 不走此分支(main.js 已 await boot 后才 $mount)
|
||||
// boot 完成后 emitUpdated 触发 $forceUpdate, 页面重新渲染显示真实翻译
|
||||
if (IS_REMOTE_ONLY && !bootCompleted) {
|
||||
return '';
|
||||
}
|
||||
|
||||
_langRecursionDepth++;
|
||||
try {
|
||||
const locale = getCurrentLocale();
|
||||
|
||||
Reference in New Issue
Block a user