Repository Wiki
jason5ng32/MyIP

主题、多语言与 PWA 体验

本页说明 MyIP 如何在首屏与运行期保持主题体验、按需加载翻译及其回退资源,以及如何提供移动端入口并清理历史 Service Worker。重点是区分当前实现与旧 PWA 缓存机制:已核实的启动调用执行的是注销与缓存清理,而不是注册离线 Worker。

目的与范围

覆盖三个相互关联的前端体验层:

  • 主题:HTML 首屏预设、系统配色偏好、Vue 生命周期内的监听与状态更新。
  • 多语言:偏好选择优先级、语言包发现、回退链并行加载、页面语言与元信息同步。
  • PWA 与移动端:manifest 声明、浏览器主题色、安全区域,以及历史离线资源迁移清理。

工具业务、网络检测接口、完整偏好存储和路由实现属于其他应用架构或工具专题,本页不展开。运行时未提供相邻 Wiki 的实际路径,因此不虚构跨页链接。

**证据边界:**本页依据四个关键实现片段与启动入口检索结果。未读取 manifest 内容、语言注册表、store 实现或自动化测试,因此不推断安装条件、完整支持语言清单、偏好写入流程或测试覆盖率。

概述

主题采用两阶段处理:在 Vue 加载前,由 index.html 同步设置根元素的 dark 类,避免暗色用户先看到浅色启动画面;运行期由 useTheme() 监听偏好和系统配色,向 store 与 body 发布当前主题。

多语言不是启动时打包注入所有消息。i18n.js 先确定语言,再加载该语言的整个回退链。这样既减少不相关语言资源进入首屏路径,也保证缺少翻译键时确实有可用的回退消息。

移动端体验由 HTML 元信息和布局规则支撑。与此同时,unregisterLegacyServiceWorker() 用于迁移历史 Serwist 客户端,注销当前可见的注册并清理可访问缓存。这与“当前应用依靠 Service Worker 离线运行”是不同的能力。

架构与职责

Loading diagram...

Sources: index.html、use-theme.js、i18n.js、main.js、unregister-service-worker.js。

这些模块并非单一配置服务:首屏脚本必须在框架启动前运行,主题协调器依赖 Vue 生命周期,多语言实例在模块初始化时确定活动语言,而旧 Worker 清理是独立的浏览器副作用。

主题:从首屏到运行期

首屏预设与容错

index.html 的同步脚本依次执行:

  1. 在根元素设置 data-booting,用于启用启动阶段样式。
  2. 定义 window.jnReadPrefs(),从 userPreferences_v7 读取 JSON;存储不可用或 JSON 损坏时返回空对象。
  3. 取 theme,缺失或假值时按 auto 处理。
  4. 显式 dark 使用暗色;auto 才查询系统偏好。
  5. 暗色时给 document.documentElement 添加 dark 类。

这是启动脚本中的真实片段:

javascript
1var theme = window.jnReadPrefs().theme || 'auto'; 2var isDark = 3 theme === 'dark' || 4 (theme === 'auto' && 5 window.matchMedia && 6 window.matchMedia('(prefers-color-scheme: dark)').matches); 7if (isDark) document.documentElement.classList.add('dark');

Source: index.html

关键约束是脚本先于内联样式和 Vue bundle 执行。首屏 CSS 使用 html.dark,不直接用操作系统媒体查询决定页面背景,以免显式浅色选择被系统暗色覆盖。浅色背景为 #ffffff,暗色背景为 #0e0e0e。参见 index.html。

运行期协调与生命周期

useTheme() 创建一个 MediaQueryList,内部 applyTheme() 使用相同的暗色判定原则:

javascript
1const applyTheme = () => { 2 const theme = store.userPreferences.theme; 3 const isDark = 4 theme === 'dark' || 5 (theme === 'auto' && mediaQueryList.matches); 6 store.setDarkMode(isDark); 7 document.body.classList.toggle('body-dark-mode', isDark); 8};

Source: use-theme.js

  • onMounted 注册系统配色 change 监听,然后立即应用一次主题。
  • watch(() => store.userPreferences.theme, applyTheme) 响应应用内偏好改变。
  • 系统变化始终触发重算,但显式 light 或 dark 的结果不会被系统值覆盖。
  • onUnmounted 移除相同的事件处理器。

源码注释要求在应用根部只调用一次,避免重复监听。该 composable 没有返回可供调用的控制对象,而是通过副作用协调状态。它本身只直接修改 body 类;setDarkMode 内部怎样同步根元素,需要结合 store 实现确认,不应把两者当作已读取的同一段逻辑。参见 use-theme.js。

边界与一致性

首屏脚本与运行期并非完全等价的容错实现:首屏对 matchMedia 存在性有检查,并为缺失主题提供 auto;运行期直接调用 window.matchMedia,直接使用 store 的主题值。如果运行期收到非 dark、非 auto 的值,会计算为浅色。这说明偏好规范化与默认值仍依赖上游 store,不能仅凭本文件断言其校验规则。

此外,首屏硬编码的 userPreferences_v7 必须与应用偏好键同步;源码已明确标出此维护约束。不同步会使首屏与挂载后的主题发生跳变。参见 index.html。

多语言:选择、加载与回退

语言包发现与选择优先级

i18n.js 通过 import.meta.glob('./*.json') 发现语言包,再与 LOCALE_CODES 取交集生成 localeLoaders。因此,“目录里有 JSON”并不自动等于“该语言可被选用”;注册表和对应资源都必须存在。

语言选择顺序为:有效已存偏好 → URL 的 hl → 浏览器语言 → FALLBACK_LOCALE。需要注意这不是所有失败都逐级继续的流水线:存在非空 hl 但无法匹配时,立即使用默认回退语言,不再尝试浏览器语言。

javascript
1const setLanguage = () => { 2 const storedLang = readStoredLang(); 3 if (storedLang) return storedLang; 4 5 const hl = new URLSearchParams(window.location.search).get('hl'); 6 if (hl) return matchLocale(hl, supportedLanguages) || FALLBACK_LOCALE; 7 8 const browserLanguage = navigator.language || navigator.userLanguage; 9 return matchLocale(browserLanguage, supportedLanguages) || FALLBACK_LOCALE; 10};

Source: i18n.js

已存语言必须精确存在于 supportedLanguages;URL 和浏览器标签则交给 matchLocale,源码注释举出了 zh-CN 和 zh-TW 的区域匹配用途。注册表具体算法未在本次证据中展开。activeLocale 在模块初始化时计算一次;文件注释说明语言设置通过持久化后重新启动页面生效,而非在这里提供热切换 API。参见 i18n.js。

消息实例与回退链

创建 vue-i18n 实例时设置 legacy: false,初始 messages 为空。每个注册语言通过 fallbackChain(code).slice(1) 计算候选回退数组,只有长度大于 1 的数组被加入专属映射,其他使用 default: [FALLBACK_LOCALE]。注释描述的区域回退示例为 zh-TW → zh → en。参见 i18n.js。

missingWarn 与 fallbackWarn 都为 false,因为不完整语言包沿链回退属于预期行为。这避免控制台噪声,但也意味着排查缺失翻译不能仅依靠这些警告。

核心加载流程

Loading diagram...

Source: i18n.js

加载逻辑的实际入口如下:

javascript
1export async function loadActiveLocaleMessages() { 2 await Promise.allSettled(fallbackChain(activeLocale).map((code) => loadOne(code))); 3 updateMeta(); 4}

Source: i18n.js

loadOne 已加载时直接返回;没有 loader 也直接返回。导入成功但无默认导出时,不注册消息、不记录为成功。正常成功后才更新 loaded 集合。allSettled 避免单个资源失败阻止后续元信息更新,但并不保证所有资源失败时仍有完整翻译。文件注释称调用方在挂载前等待加载;本次未读取完整启动顺序,因此不进一步推断初始化屏障和其他任务的先后关系。

页面语言与元信息同步

updateMeta() 设置 HTML 的 lang,再翻译 page.title、page.keywords、page.description。后两项仅在对应 meta 元素存在时更新。使用 toHtmlLang 而非直接复制内部 locale,能够表达更精确的网页语言标签;注释指出简体中文应声明为 zh-CN,以避免部分系统采用不合适的汉字字体回退。

这个同步也服务于浏览器自动翻译和屏幕阅读器:静态 HTML 初始为英文,如果实际中文页面仍声明英文,可能引发重复自动翻译。此函数没有更新 Open Graph、Twitter 或 JSON-LD 的语言和文案;这些静态元信息不能据此声称已与当前语言全面同步。参见 i18n.js 与 index.html。

PWA 与移动端体验:保留入口,清理历史缓存

HTML 提供的移动端能力

index.html 声明 manifest URL /manifest.webmanifest,并配置 Apple touch icon。相关元信息为:

html
1<meta name="mobile-web-app-capable" content="yes" /> 2<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> 3<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" /> 4<meta name="theme-color" content="#0e0e0e" media="(prefers-color-scheme: dark)" /> 5<meta name="background-color" content="#ffffff" /> 6<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

Source: index.html

注意 theme-color 根据系统媒体查询选择,而正文根据应用主题选择。这两条路径在所读代码中没有统一更新逻辑:用户显式选择与系统相反的主题时,不能保证浏览器界面颜色与正文一致。

页面使用 viewport-fit=cover 并为 body 预留 calc(env(safe-area-inset-top) + 3.5rem),兼顾 iOS 安全区域与首页固定导航占位。body.jn-standalone-page 将此 padding 归零;注释说明独立页面标题自行处理安全区域。具体路由如何切换这个类不在本页证据范围内。参见 index.html。

manifest 的存在性引用不是离线功能证明。本次没有读取 manifest 内容,无法确认 display、start_url、scope、完整图标列表或安装提示交互。

历史 Service Worker 的迁移流程

启动入口检索确认 main.js 调用了 unregisterLegacyServiceWorker()。清理函数具有三个前置条件:生产模式、navigator 存在、支持 serviceWorker。不满足任一条件就整体返回,连缓存清理也不执行。

通过检查后,函数分别启动两条异步链:

javascript
1navigator.serviceWorker.getRegistrations() 2 .then((regs) => Promise.all(regs.map((r) => r.unregister()))) 3 .catch(() => {}); 4 5if (typeof caches !== 'undefined') { 6 caches.keys() 7 .then((keys) => Promise.all(keys.map((k) => caches.delete(k)))) 8 .catch(() => {}); 9}

Source: unregister-service-worker.js

这里有几个重要的运维语义:

  • **清理不按名称过滤。**所有返回的注册都会被注销,所有返回的缓存名都会被删除;不是仅匹配 Serwist 前缀。
  • **两条链没有先后屏障。**代码不会等待注销完成再删除缓存。
  • **调用者无法等待整体完成。**函数不是 async,也没有返回这两条 Promise。
  • **失败不阻断启动,也不记录日志。**两个异步链都以空 catch 结束。
  • **“一次性迁移”是用途描述,不是执行次数保证。**实现没有本地完成标记,每次满足条件的调用都会再次枚举。

文件注释把历史预缓存规模描述为约 5 MB,并建议经过几个发布周期、陈旧客户端更新后移除该文件及启动调用。这是源码中的历史估计与维护建议,不是当前部署缓存规模的测量结果。参见 unregister-service-worker.js。

配置与输入参考

下表区分明确写在代码中的值与依赖外部模块的值,避免把启动兜底误认为全局默认配置。

配置或输入类型已核实默认值或行为影响
首屏存储键stringuserPreferences_v7jnReadPrefs() 读取的偏好条目,须与应用常量同步
themestring首屏缺失时 auto;运行期默认值未核实dark 强制暗色,auto 跟随系统,其他值按当前判定得到浅色
prefs.langstring无有效值则继续选择必须精确存在于实际 loader 列表
URL hlstring缺失或空值时尝试浏览器语言非空且匹配失败时直接使用默认回退语言
navigator.language / userLanguagestring匹配失败使用 FALLBACK_LOCALE浏览器语言输入
LOCALE_CODES语言代码集合定义未读取与 JSON 包发现结果共同限定可选资源
FALLBACK_LOCALEstring注释描述为 en,常量定义未读取默认回退语言
legacybooleanfalsei18n 实例配置
missingWarn / fallbackWarnboolean均为 false静默处理缺键与正常回退警告
import.meta.env.PRODboolean由构建环境提供历史 Worker 清理仅在生产模式执行
manifest URLstring/manifest.webmanifestHTML 引用的应用清单入口

依据:index.html、index.html、i18n.js、unregister-service-worker.js。

模块 API 参考

这些是前端 JavaScript 模块入口,不是 HTTP API;源文件没有 TypeScript 参数或返回值注解。

入口参数与结果调用约束与副作用
useTheme()无参数;没有显式返回值在 Vue 生命周期上下文中使用;创建系统监听与偏好 watcher,更新 store 和 body;直接依赖浏览器 window
loadActiveLocaleMessages()无参数;异步完成,无业务返回值加载模块初始化时选定语言的回退链,然后更新页面元信息
默认导出 i18ncreateI18n(...) 创建的实例初始消息为空;消息由加载入口填充
unregisterLegacyServiceWorker()无参数;没有显式返回值生产环境启动异步清理,不返回完成信号
window.jnReadPrefs()无参数;正常返回解析后的存储值,失败或空值返回 {}在首屏脚本定义,捕获存储访问和 JSON 解析异常;没有对象结构校验

接口依据:use-theme.js、i18n.js、unregister-service-worker.js、index.html。

内部 setLanguage()、loadOne(locale)、updateMeta() 和 applyTheme() 均不是上述模块的命名导出,不应作为稳定外部接口使用。

故障、并发与性能注意事项

多语言初始化并非所有错误都被吞掉

readStoredLang() 的 JSON 解析在 try 内,但 localStorage.getItem(PREFS_STORAGE_KEY) 在 try 外。因此,损坏 JSON 会被忽略,存储访问本身抛出的异常却可能使模块初始化失败。这与首屏 jnReadPrefs() 的保护范围不同。参见 i18n.js。

Promise.allSettled 只容忍语言加载任务的拒绝;之后的 updateMeta() 不在单独的容错块中。缺少浏览器 DOM 等环境问题仍可能让公开异步函数拒绝。也不要将 Vite glob 的 try/catch 理解为整个模块支持无浏览器执行:选择语言仍依赖浏览器全局对象。

缓存的是已完成状态,不是进行中的 Promise

loaded 集合只有在 setLocaleMessage 成功后才记录语言。重复的顺序调用可以跳过已成功加载的语言;重叠调用则可能在成功标记写入前重复进入 loader。本模块没有独立的进行中任务去重。缺失默认导出或加载失败不会写入成功标记,未来再次调用入口时仍有机会重试,但源码没有定时重试或退避策略。参见 i18n.js。

按需加载并不意味着每次只下载一个包:活动语言的整个回退链都会启动加载。源码注释提到过去同时加载四种语言带来约 44 KB gzip 的无用资源;这属于历史优化背景,不应当作当前精确体积。安全清单数据也被明确排除在此加载路径之外。参见 i18n.js。

主题与清理的运行约束

主题协调器多次实例化会产生重复系统监听;应遵守单根实例约定。主题系统没有在已读片段中注册跨标签页的 storage 监听,因此不能宣称偏好修改会实时同步到所有标签页。

Worker 清理是无命名过滤的操作。与其他应用共用来源部署时,需要检查是否会删除共享来源下其他缓存。又因为清理错误静默处理,排查旧资源问题应在浏览器开发者工具中检查实际注册与 Cache Storage,而不能只根据启动函数已经调用就断言迁移完成。

扩展与验证建议

以下是根据实现提出的检查建议,不代表仓库已有自动化测试覆盖:

  1. **扩展主题选项:**同时审查首屏判定与 applyTheme(),否则挂载前后可能使用不同主题;变更存储版本时同步首屏硬编码键。
  2. **添加语言:**语言包发现结果必须与注册表一致;还要核对回退链和 toHtmlLang 映射。仅添加 JSON 不足以保证语言进入可选列表。
  3. **修改翻译键:**保留或同步维护 page.title、page.keywords、page.description,因为它们不仅用于组件,还用于页面元信息。
  4. **调整 PWA 策略:**在重新引入 Worker 或其他缓存前,先处理当前全量注销和删除逻辑,否则新能力可能与迁移清理冲突。
  5. **主题验证矩阵:**覆盖 auto/light/dark 与系统明暗组合、首次访问、损坏存储、偏好切换、系统切换及组件卸载。
  6. **语言验证矩阵:**覆盖已存偏好压过 hl、未知 hl 直接回退、区域标签、缺包、加载拒绝、缺默认导出,以及 lang 和 meta 更新。
  7. **清理验证矩阵:**覆盖开发模式跳过、浏览器不支持、无缓存、多缓存、异步失败和重复调用。

本次未读取测试实现,不对这些场景给出“测试已通过”或“已有保障”的结论。

相关链接

Sources

(4 files)
frontend/composables
frontend/locales