应用外壳、路由与独立工具页面
MyIP 将跨页面能力放在轻量根组件 App 中,通过 Vue Router 切换首页、独立工具页、隐私页和共享报告页。工具既可以通过首页查询参数打开抽屉,也可以通过 /tools/:slug 进入独立页面;独立页复用工具组件,而不是复制工具实现。
目的与范围
本页面向需要修改页面结构、增加导航入口或排查独立工具访问问题的开发者,覆盖:
- 根组件提供的全局 UI、主题初始化、访问上报与数据收集入口。
- 路由匹配、按需加载、滚动位置和未知路径处理。
- 独立工具页面的注册信息读取、组件加载、页面元信息与回退规则。
- 当前实现的生命周期边界,以及扩展时需要特别验证的行为。
工具自身的检测算法、用户登录与配额后端、报告存储、主题持久化、PWA 安装资格算法不在本页展开,应分别归入对应功能文档。当前未提供其他目录项的实际路径,因此不构造未经核实的 Wiki 链接。
本文主要依据 App.vue、index.js 和 StandaloneTool.vue。工具注册表、首页抽屉、应用启动入口及元信息组合函数的内部实现未在本次有限源码阅读中核实,相关内容只描述已见调用契约。
概览
页面结构存在三个不同层次:
| 层次 | 主要职责 | 生命周期或切换边界 |
|---|---|---|
App | Tooltip 上下文、Toast、文档助手、PWA 提示、主题与全局收集器 | 根组件初始化与挂载 |
| Vue Router | 根据路径选择页面,控制导航滚动行为 | 路由导航 |
StandaloneTool | 根据工具注册信息渲染页头、标题、工具正文、页脚和用户系统宿主 | 独立工具页面实例 |
这里的“独立页面”仍是同一个 Vue SPA 的路由页面,不意味着另一个前端应用。jn-standalone-page 只是页面布局标记,也不等同于 PWA 的独立窗口显示模式。
首页路由注释约定 ?tool=<slug> 由 Advanced.vue 处理,独立工具路由则使用 /tools/:slug。两者复用相同工具组件、采用不同容器;依赖首页状态的工具通过 noStandalone 禁止进入独立容器。依据:路由入口说明、独立页约束。
架构
Sources: App.vue、index.js、StandaloneTool.vue。
App 不直接渲染首页工具集合,而是将页面主体交给 router-view。因此,独立访问工具和共享报告时仍然拥有全局 Toast、文档助手和主题能力,不需要各页面重复声明这些根级能力。相反,独立工具专用的 User 宿主、页头和页脚放在 StandaloneTool 内,避免把页面级容器提升为所有页面的固定外壳。
根应用外壳与生命周期
全局 UI 与提示上下文
以下为根模板原始实现:
1 <TooltipProvider :delay-duration="150">
2 <router-view />
3 <Alert />
4 <DocsAssistant />
5 <PWA v-if="offerPwaInstall" />
6 </TooltipProvider>Source: App.vue。
TooltipProvider 包裹页面内容和全局挂件,提示延迟在根层统一设为 150。Alert 实际导入的是 Toast 组件;不要把这个名称误认为页面内的普通警告面板。PWA 则受条件控制,并非每个访问者都加载。
PWA 提示与访问上报
根组件把 PWA 定义为异步组件,offerPwaInstall 初始为 false。在 onMounted 中:
- 调用
shouldOfferPwaInstall()判断是否有资格展示安装提示。 - 只有判断通过时才启动 30 秒定时器。
- 定时器将开关设为
true,模板才开始渲染异步PWA。 - 无论资格判断结果如何,随后都调用
sendVisitBeacon()。
源码注释将资格规则概括为访问次数、提示上限和已安装状态,但具体阈值与存储方式未核实。访问上报放在根挂载阶段,意图是每次页面加载记录一次,而不是每次 SPA 路由切换都发送;服务端 IP 去重是调用处注释描述的契约,并非本页验证过的后端实现。依据:App.vue。
路由驱动的布局标记
1const STANDALONE_ROUTES = new Set(['tool', 'privacy', 'report']);
2const route = useRoute();
3watch(
4 () => STANDALONE_ROUTES.has(route.name),
5 (isStandalone) => {
6 document.body.classList.toggle('jn-standalone-page', isStandalone);
7 },
8 { immediate: true },
9);Source: App.vue。
这里按路由名称而不是路径前缀判断布局。观察的是“是否属于独立页集合”这一布尔值;从 tool 切换到 privacy 时,值仍为 true,无需反复修改类名。immediate: true 使首次直接打开独立链接也能及时获得布局标记。
调用处注释说明,该标记用于移除首页固定导航对应的 body 上边距,避免独立页自己的页头上方出现空白。CSS 具体规则未在本次阅读中核实。新增带独立页头的路由时,需要同时考虑这组路由名称,而不只是增加路由表记录。
启动遮罩交接与全局初始化
根组件读取 jn-loading 和 app DOM 节点,然后:
- 移除根元素的
data-booting属性。 - 若存在
app,通过requestAnimationFrame添加jn-app-enter。 - 若存在启动遮罩,在动画帧中添加
jn-loading-stage-1,立即添加jn-loading-stage-2,200 毫秒后移除遮罩。
需要区分注释中的“根挂载交接”与代码执行位置:revealApp() 和遮罩处理直接位于 <script setup> 顶层,并没有包在 onMounted 回调中。节点缺失时有空值保护,但代码仍直接访问 document,所以这段实现依赖浏览器环境。
随后根组件依次调用 useTheme()、useAchievementEngine()、useReportCollector() 和 useAppPersonaCollector()。调用处注释分别描述主题编排、领域事件成就判定、测试结果报告快照及 Persona 标准化快照收集。它们放在根层,有利于工具页面之间共享应用级收集过程;具体监听器、缓存结构和清理方式没有在此处展示,不应据此推断持久化保证。依据:App.vue。
路由表与导航语义
页面入口
| 路径 | 路由名 | 页面组件 | 导入策略 | 作用 |
|---|---|---|---|---|
/ | home | Home | 静态导入 | 默认首页;抽屉通过查询参数表达 |
/tools/:slug | tool | StandaloneTool | 动态导入 | 独立工具页面 |
/privacy | privacy | PrivacyPolicy | 动态导入 | 隐私说明页面 |
/r/:id | report | ReportPage | 动态导入 | 共享诊断报告入口 |
/:pathMatch(.*)* | 未设置 | 重定向至 / | 不适用 | 未知路径回退 |
Home 提前导入,其他页面通过函数返回动态 import()。源码明确将此安排用于默认落地页与其他页面的加载隔离。报告路由注释描述其为只读、KV 支持、noindex,但这些行为在报告页及后端中的实现不属于本页已验证范围。依据:index.js。
History 模式与滚动优先级
1const router = createRouter({
2 history: createWebHistory(),
3 routes,
4 scrollBehavior(to, from, savedPosition) {
5 // Opening/closing the drawer only flips the query on the home route — don't
6 // scroll the homepage in that case. Genuine page changes go to the top.
7 if (to.path === from.path) return false;
8 if (savedPosition) return savedPosition;
9 return { top: 0 };
10 },
11});Source: index.js。
判定顺序有实际意义:
- 路径不变优先:即使存在
savedPosition,只要to.path === from.path,仍返回false,不主动滚动。这覆盖首页打开、关闭抽屉这类仅改变查询参数的导航,也覆盖其他同路径查询参数或 hash 变化。 - 跨路径的历史恢复:如果路径变化且存在
savedPosition,恢复该位置。 - 其余跨页导航:滚动到页面顶部。
路由使用 createWebHistory(),没有在此显式传入 base,也没有配置 hash 模式。生产环境直接请求深层 URL 时,需要确保服务器能把前端页面交给 SPA;本页没有读取静态服务器实现,因此不声明具体回退规则或部署配置已经满足该条件。
独立工具页:从 slug 到实际工具组件
注册信息与独立运行资格
StandaloneTool 调用 useRoute()、useRouter() 和 useI18n(),然后执行 TOOL_BY_SLUG.get(route.params.slug) || null。这里可确认注册表提供 .get() 查询能力;其定义和完整工具集合未在本次阅读中核实。
页面消费的注册项字段如下:
| 字段 | 在本页中的用途 | 边界 |
|---|---|---|
slug | canonical 路径以及回退首页的 tool 查询值 | 读取失败时不使用 |
noStandalone | 真值时拒绝独立容器 | 不是用户权限校验 |
component | 传给异步组件的 loader | 具体加载器由注册项提供 |
emoji | 页头和正文标题中的工具标识 | 正文中标记 aria-hidden="true" |
titleKey | 经 t() 翻译后用于页头、h1 和文档标题 | 依赖语言资源 |
noteKey | 经 t() 翻译后用于 description | 元信息更新机制交给组合函数 |
noStandalone 的设计约束是工具依赖首页状态:此时不能只把原组件塞进独立容器,而要把访问者带回首页抽屉。它不是鉴权机制,也不能替代服务端访问控制。依据:StandaloneTool.vue、注册项读取与元信息。
实际加载代码
1const registered = TOOL_BY_SLUG.get(route.params.slug) || null;
2const tool = computed(() => (registered && !registered.noStandalone ? registered : null));
3
4// The skeleton covers the chunk download — same treatment as the homepage
5// drawer; `delay` keeps fast loads flash-free.
6const toolComponent = computed(() => (tool.value
7 ? defineAsyncComponent({
8 loader: tool.value.component,
9 loadingComponent: ToolLoadingSkeleton,
10 delay: 200,
11 })
12 : null));Source: StandaloneTool.vue。
有效工具通过 defineAsyncComponent 生成可渲染组件,模板再用 <component :is="toolComponent"> 输出。ToolLoadingSkeleton 是工具分块下载过程的加载占位;200 毫秒是显示占位的延迟,而非网络超时。这样快速加载时不会立刻闪现骨架。
整个过程有两层异步边界:路由先加载 StandaloneTool,随后独立页再加载注册项中的实际工具组件。本页的骨架只属于第二层,不能据此推断首次进入独立路由时也一定由它占位。
容器组成与用户系统宿主
独立页使用纵向 flex 容器和 min-h-screen,正文占用剩余空间。正文内部最大宽度为 1400px,包含工具 h1 和动态组件,下方是 Footer。
顶部 StandalonePageHeader 接收组合后的工具 emoji 与翻译标题。User 同样在独立页挂载;模板注释解释它承担 Benefits & Usage 对话框宿主,并获取用户用量快照,使工具的前端配额提示和门控在独立页也有支撑。这里核实的是宿主接入,不是具体鉴权、请求时机或用量计算。依据:StandaloneTool.vue。
元信息与分享地址
1useDocumentMeta(() => {
2 if (!tool.value) return {};
3 return {
4 title: `${t(tool.value.titleKey)} · IPCheck.ing`,
5 description: t(tool.value.noteKey),
6 canonical: `${window.location.origin}/tools/${tool.value.slug}`,
7 };
8});Source: StandaloneTool.vue。
页面把回调交给 useDocumentMeta:标题带产品后缀,描述来自工具说明键,canonical 使用当前站点 origin 和注册项 slug,不带当前 URL 的查询参数。无有效工具时返回空对象。
这里只能确认传入的字段,不能推断组合函数如何响应语言切换、是否清理上一个页面的标签或是否设置 Open Graph 元信息。此外,canonical 固定使用根路径 /tools/;如计划把站点部署在子路径下,需要联合核查路由 base 与 canonical 构造。
核心访问流程与回退
Source: StandaloneTool.vue。
回退的原始实现为:
1if (!tool.value) {
2 router.replace(registered?.noStandalone
3 ? { path: '/', query: { tool: registered.slug } }
4 : '/');
5}Source: StandaloneTool.vue。
这是两种不同错误边界:
- 未知工具:
/tools/:slug已匹配路由,但是注册表没有该工具,独立页主动回到/。 - 已知但不允许独立运行:保留工具意图,转为首页的
?tool=<slug>。首页抽屉如何响应该查询参数,仍属于未展开的实现。
二者都使用 replace 而非 push,避免通过主动新增历史记录把无效独立页作为一个额外返回点。全站未知路径则走路由表的 catch-all redirect;不要把它与独立页内部的 slug 检查混为一谈。
配置与调用契约
本页没有发现独立的环境变量配置接口。下表均为已读取实现里的代码级常量或输入,而非可通过环境变量直接覆盖的配置:
| 选项 | 类型 | 默认或当前值 | 影响 |
|---|---|---|---|
Tooltip delay-duration | 数值 | 150 | 全局提示延迟 |
offerPwaInstall | 响应式布尔值 | false | 控制 PWA 组件是否渲染 |
| 安装提示延迟 | 毫秒数 | 30 * 1000 | 通过资格检查后等待时间 |
| 遮罩移除延迟 | 毫秒数 | 200 | 启动遮罩 DOM 移除时间 |
STANDALONE_ROUTES | Set | tool、privacy、report | 页面布局类名判定 |
工具骨架 delay | 毫秒数 | 200 | 工具异步加载占位显示延迟 |
| 路由 history | History 实例 | createWebHistory() | 使用非 hash 的路径导航 |
route.params.slug | 路由参数 | URL 提供 | 工具注册项查询键 |
注册项 noStandalone | 按真值判定 | 未显式设置默认值 | 真值时回退首页抽屉 |
依据:App.vue、index.js、StandaloneTool.vue。
本能力的主要对外契约是前端 URL,而不是 HTTP API:/tools/:slug 选择独立工具,/?tool=<slug> 表达首页抽屉目的地。已读取页面没有定义业务 REST 端点或数据库写入。
就本地函数而言,scrollBehavior(to, from, savedPosition) 接收 Vue Router 的导航上下文,返回 false、原始 savedPosition 或 { top: 0 };revealApp() 无参数,负责 DOM 启动状态切换,没有显式返回值。二者没有显式 throw。其他导入组合函数只核实了调用点,本文不猜测其完整签名、返回值与异常类型。
边界、失败模式与并发注意事项
同一路由修改 slug 的响应性风险
当前 registered 是在 setup 时直接赋值的普通常量,不是根据 route.params.slug 计算的响应式引用。tool 虽然是 computed,其内部读取的却仍是这个常量;回退分支也只在 setup 中执行一次。
因此,如果导航在同一个 StandaloneTool 实例内把一个 slug 改为另一个 slug,这段代码本身不会重新查询注册表。已读取 App 中的 <router-view /> 也没有显式 key。这是应重点验证的组件复用边界,不宜误认为使用了 computed 就已保证 slug 切换正常。可在扩展时选择让注册项查询响应参数变化,或者明确管理页面重建;具体修复方案应结合完整导航链路测试,而不是在文档中假定已有实现。依据:App.vue、StandaloneTool.vue。
异步分块失败
独立工具异步组件选项只有 loader、loadingComponent 和 delay,没有配置 errorComponent、timeout 或 onError 重试策略。路由动态导入同样没有在该文件配置局部异常处理。因此源码没有证明存在自动重试或专用错误页,骨架也不等于失败恢复机制。
定时器与根实例边界
根层 30 秒安装提示和 200 毫秒遮罩移除定时器未保存句柄,已读取文件中没有对应的卸载清理。正常页面切换不应被当作再次执行根初始化的契机;如果测试或嵌入场景会卸载并重建根应用,应额外核查定时器和全局组合函数的清理行为。
这些代码主要涉及 UI 生命周期,不提供事务、跨标签页锁或请求去重实现。不能因为收集器位于根组件,就推断内部事件处理天然无重复或已持久化。
性能、运维与扩展建议
- 维持分层按需加载:非首页页面采用路由动态导入,实际工具再经注册项 loader 加载。增加工具时不应为了方便把工具实现改成根组件静态导入。
- 区分全局与页面级依赖:必须覆盖所有路由的能力接入
App;只服务独立工具容器的 UI 保持在StandaloneTool,避免所有页面都承担额外宿主。 - 新增独立页应检查三处契约:路由表的名称与路径、根层独立布局集合、页面自己的页头与元信息。只添加路由不保证布局正确。
- 新增工具先判断是否依赖首页状态:当前代码明确支持
noStandalone回退。注册表定义未在本页展开,所以具体注册格式应以该实现为准,不提供虚构的新增工具代码。 - 部署时检查深链访问:分别验证直接打开、刷新和站内导航进入
/tools/:slug,不要仅用从首页点击成功来判定 History 路由部署正确。 - SEO 需要分开验证:页面的
h1与元信息回调已经存在,但 SSR、预渲染和抓取结果未被本次源码阅读证明。
验证建议与证据限制
本次没有读取对应自动化测试,也没有执行浏览器验证或构建,不能宣称以下场景已有测试覆盖。建议至少验证:
| 场景 | 按已见代码应检查的结果 |
|---|---|
首页仅修改 tool 查询参数 | 不主动重置首页滚动位置 |
| 跨页面普通导航 | 无保存位置时回到顶部 |
| 跨页面历史返回 | 有保存位置时恢复位置 |
| 首次打开独立工具、隐私或报告页 | body 获得独立布局类名 |
| 回到首页 | 独立布局类名被移除 |
| 未注册 slug | 替换为首页地址 |
noStandalone 工具 | 替换为带工具查询参数的首页地址 |
| 慢速工具分块加载 | 超过骨架显示延迟后展示占位 |
| 工具 A 到工具 B 的站内导航 | 检查是否复用实例以及是否出现旧工具残留 |
| 不具备安装提示资格 | offerPwaInstall 不开启 |
项目级贡献说明列出 Node 内置测试运行器,并将 UI 渲染、真实网络与浏览器 API 排除在其常规单测范围之外;这不等于已经验证本页的浏览器行为。依据:AGENTS.md。
相关链接
- 根级初始化和布局调整:参见 App.vue 的主题、收集器与启动交接。
- 导航和 URL 入口调整:参见 index.js 的路由与滚动策略。
- 工具容器与回退调整:参见 StandaloneTool.vue 的注册项查询和渲染流程。
这些链接用于定位本页所讨论机制的修改点;工具算法、账户配额、报告后端与部署实现仍应在各自专题中单独阅读。