From eef59211d310034df318b6ab4d200310e4d4399a Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 30 Jun 2026 15:29:45 +0800 Subject: [PATCH] =?UTF-8?q?refactor(i18n):=20=E5=AE=8C=E5=96=84remote-only?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=8B=E7=9A=84=E9=A6=96=E5=B1=8F=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=92=8C=E8=AF=B7=E6=B1=82=E9=97=A8=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- App.vue | 51 ++++++++++++++++++++----------- common/js/golbalConfig.js | 8 +++++ common/js/http.js | 29 ++++++++++++++++++ common/js/lang-loader.js | 1 + common/js/lang.js | 63 ++++++++++++++++++++++++++++++++------- main.js | 44 ++++++++++++++++----------- 6 files changed, 152 insertions(+), 44 deletions(-) diff --git a/App.vue b/App.vue index 8795542..b7e119d 100644 --- a/App.vue +++ b/App.vue @@ -42,24 +42,41 @@ try { uni.$emit && uni.$emit('uniacid:changed'); } catch (e) {} } - // remote-only 模式下 main.js 已先 await Lang.boot() 再挂载 Vue,这里复用其结果 - // 非 remote-only 模式下仍需主动 boot 以拉取远端覆写 - const langBoot = Lang.boot(); - this.$langReady = langBoot; + // remote-only 模式: + // H5: main.js 已 await boot 完成, 这里秒回 + // 小程序/鸿蒙: main.js 同步 $mount(), boot 在这里异步执行 + // → boot 期间 showLoading 遮罩, 完成后 hideLoading + const langBoot = Lang.boot(); + this.$langReady = langBoot; + let _hideLangLoading = false; + // #ifndef H5 + if (Lang.isRemoteOnly && Lang.isRemoteOnly()) { + try { + uni.showLoading({ title: '加载中...', mask: true }); + _hideLangLoading = true; + } catch (e) {} + } + // #endif - // ---- 2) 主题应用(同步 await) + 远端拉取(并行 await ) - // 关键: 这一步必须 await,否则 page 会在主题挂上之前就渲染 - // 内部行为: 第一次 apply 只用 preset(此时 remote 还没拉),远端就绪后第二次 apply 叠加 - // 并行把 Lang.boot / ThemeOverride.boot 也拉了,节省总时间 - await Promise.all([ - // A) 主题第一次 apply — 走 preset,失败静默走默认 - this.$store.dispatch('applyThemeWithRemote', presetName) - .catch(e => console.warn('[theme] pre-apply fail', e)), - // B) Lang boot(首屏强依赖,remote-only 下已提前完成) - langBoot.catch(e => console.warn('[i18n] boot fail', e)), - // C) ThemeOverride boot(brand/light/dark 三桶,bootTimeout=2000ms) - ThemeOverride.boot().catch(e => console.warn('[theme] boot fail', e)), - ]); + // ---- 2) 主题应用(同步 await) + 远端拉取(并行 await ) + // 关键: 这一步必须 await,否则 page 会在主题挂上之前就渲染 + // 内部行为: 第一次 apply 只用 preset(此时 remote 还没拉),远端就绪后第二次 apply 叠加 + // 并行把 Lang.boot / ThemeOverride.boot 也拉了,节省总时间 + await Promise.all([ + // A) 主题第一次 apply — 走 preset,失败静默走默认 + this.$store.dispatch('applyThemeWithRemote', presetName) + .catch(e => console.warn('[theme] pre-apply fail', e)), + // B) Lang boot + langBoot.catch(e => console.warn('[i18n] boot fail', e)), + // C) ThemeOverride boot(brand/light/dark 三桶,bootTimeout=2000ms) + ThemeOverride.boot().catch(e => console.warn('[theme] boot fail', e)), + ]); + + // #ifndef H5 + if (_hideLangLoading) { + try { uni.hideLoading(); } catch (e) {} + } + // #endif // ---- 3) 远端就绪后,主题再 apply 一次(preset + remote 合并 ) // 这一次 setThemeStyle + themeColorSet 是同步路径(preset 已在 cache,remote 也在 cache) diff --git a/common/js/golbalConfig.js b/common/js/golbalConfig.js index a642f2b..147050c 100644 --- a/common/js/golbalConfig.js +++ b/common/js/golbalConfig.js @@ -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); diff --git a/common/js/http.js b/common/js/http.js index ac5d584..483636e 100644 --- a/common/js/http.js +++ b/common/js/http.js @@ -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(/(?} */ - 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(); diff --git a/main.js b/main.js index ec645ff..ea83253 100644 --- a/main.js +++ b/main.js @@ -14,18 +14,18 @@ import globalConfig from './common/js/golbalConfig.js'; import { uniStorage } from './common/js/storage.js' -import configExternal, { langConfig } from './common/js/config-external.js' - -Vue.prototype.$store = store //挂在vue - -Vue.config.productionTip = false - -Vue.prototype.$util = Util; -Vue.prototype.$api = Http; - -Vue.prototype.$langConfig = Lang; //语言包对象 -Vue.prototype.$lang = Lang.lang; //解析语言包 - +import configExternal from './common/js/config-external.js' + +Vue.prototype.$store = store //挂在vue + +Vue.config.productionTip = false + +Vue.prototype.$util = Util; +Vue.prototype.$api = Http; + +Vue.prototype.$langConfig = Lang; //语言包对象 +Vue.prototype.$lang = Lang.lang; //解析语言包 + Vue.prototype.$config = Config; @@ -55,14 +55,22 @@ uniStorage(); // 组件已改为按需引入,不再全局注册 -// H5 平台: remote-only 下先 await Lang.boot() 再挂载 Vue,避免首屏 raw key -// 小程序/鸿蒙: 同步注册 App,Lang.boot() 在 App.onLaunch 中异步执行(不阻塞挂载) -// 原因: 小程序/鸿蒙的 App() 必须同步注册,async bootstrap 会导致 App 注册延迟,白屏 +// ============================================================ +// 平台差异处理: +// H5: 可在 $mount() 前 await boot, 首屏直接有翻译 +// 小程序/鸿蒙: App() 必须同步注册, $mount() 不能推迟 +// → 同步 $mount() + lang() 短路返回空串 + boot 完成后 $forceUpdate +// +// 以下代码所有平台共享: +// - lang.js: bootCompleted 标志, boot 未完成时 lang() 返回空串 +// - http.js: sendRequest 门控, boot 未完成时业务请求排队 +// - App.vue: onLaunch 中 showLoading + await boot + hideLoading +// ============================================================ // #ifdef H5 +// H5: remote-only 下先 await boot 再 $mount(), 首屏直接有翻译数据 async function bootstrap() { - const isRemoteOnly = langConfig && langConfig.source === 'remote-only'; - if (isRemoteOnly) { + if (Lang.isRemoteOnly && Lang.isRemoteOnly()) { let _hideLoading = false; try { uni.showLoading({ title: '加载中...', mask: true }); @@ -89,6 +97,8 @@ bootstrap() // #endif // #ifndef H5 +// 小程序/鸿蒙: 同步 $mount(), boot 在 App.vue onLaunch 中异步执行 +// lang() 在 boot 未完成时返回空串, boot 完成后 emitUpdated 触发 $forceUpdate const app = new Vue({ ...App, store