Day 78 · Vue Router 基础 — 三屏路由化:URL 成为状态
今天让 Day 64 手写的
ScreenRouter开始退役:三屏切换交给 Vue Router,URL 成为屏幕状态的一部分——/a/b/c分别对应三屏,刷新不丢、后退可用、可收藏分享。这是替换周的第一刀:只动路由,数据层(useScreenStore)原样服役,替换后跑联调清单验证"行为零变化"(URL 行为除外——这是本周唯一允许的改进)。
目录
- 一、安装与路由表
- 二、router-view:单挂载点的世界
- 三、轮播指示器与 router-link
- 四、与 v-show 时代的本质差异
- 五、404 与重定向
- 六、常见坑点
- 七、自测挑战
- 八、总结
一、安装与路由表
npm install vue-router@4
// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";
/**
* 路由表:三屏 = 三条路由
* 设计决策:
* 1. path 短名(/a /b /c)——大屏是演示场景,URL 简短即专业
* 2. history 模式(createWebHistory)——URL 干净;需要服务器配置回退(见坑 4)
* 3. name 字段必写——轮播调度器按 name 切换,与 path 解耦(改路径不动逻辑)
*/
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: "/", redirect: "/a" }, // 根路径 → 屏 A
{ path: "/a", name: "overview", component: () => import("../screens/ScreenA.vue") },
{ path: "/b", name: "monitor", component: () => import("../screens/ScreenB.vue") },
{ path: "/c", name: "topology", component: () => import("../screens/ScreenC.vue") },
{ path: "/:pathMatch(.*)*", redirect: "/a" }, // 兜底(第五节)
],
});
// src/main.ts —— 注册路由(先主题 → 再路由 → 后挂载)
import { createApp } from "vue";
import { createPinia } from "pinia"; // Day 80 再启,今天先注释
import App from "./App.vue";
import { router } from "./router";
import "./styles/tokens.css";
createApp(App).use(router).mount("#app");
注意路由的懒加载写法(
() => import(...)):三屏各自分包,首屏只加载屏 A 的 chunk——原版三屏全量加载的改进,记进对照表。
二、router-view:单挂载点的世界
2.1 App.vue 的改造
<!-- App.vue:从"三屏并存 v-show"变为"单挂载点" -->
<script setup lang="ts">
// scale 壳逻辑原样保留(Day 71 的 onMounted/onUnmounted 成对纪律不变)
</script>
<template>
<div class="screen-frame">
<div class="screen-canvas" :style="{ transform: `scale(${scale})` }">
<header class="top-bar">
<h1>智慧工厂监控中心</h1>
<ConnIndicator :state="connState" />
</header>
<!-- 路由出口:当前路由的屏幕组件在这里渲染(同时只有一个) -->
<router-view />
<!-- 底部指示器(第三节) -->
<ScreenNav />
</div>
</div>
</template>
2.2 单挂载点带来的行为变化(今天必须理解的事)
v-show 时代(Week 11):三屏始终挂载,ECharts 实例常驻,切屏零成本。
router-view 时代:切屏 = 卸载旧屏 + 挂载新屏:
切到 /b:
ScreenA unmount → VChart dispose → ECharts 实例销毁
ScreenB mount → VChart init → ECharts 实例重建(约 100-200ms)
这正是 Day 76 坑 1 的场景放大版。本周的解法是 Day 82 的 KeepAlive(缓存组件实例,切走不销毁)。今天先接受"切换略重",把路由链路跑通;明天过渡 + 后天缓存,三步走完整个替换。
🎯 心智对照:原生版 Day 64 选"opacity 互斥保实例",本质就是手动实现的 KeepAlive。今天体验"没有缓存的痛",后天才会真正理解 KeepAlive 解决什么。
三、轮播指示器与 router-link
<!-- src/components/ScreenNav.vue -->
<script setup lang="ts">
/**
* 底部屏幕指示器:圆点 + 手动切换
* 迁移自原生版 Day 64 的圆点指示器
*/
import { computed } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
// 当前屏的 name(路由表里定义的),用于高亮
const current = computed(() => route.name);
// 三屏配置:单一来源,轮播调度器(Day 79)也读它
export const SCREENS = [
{ name: "overview", label: "总览", dwell: 15 },
{ name: "monitor", label: "监控", dwell: 20 },
{ name: "topology", label: "拓扑", dwell: 15 },
] as const;
</script>
<template>
<nav class="screen-nav">
<!-- router-link:声明式导航,激活态自动加 class -->
<router-link
v-for="s in SCREENS"
:key="s.name"
:to="{ name: s.name }"
class="screen-nav__dot"
:class="{ 'is-active': current === s.name }"
:title="s.label"
/>
</nav>
</template>
<style scoped>
.screen-nav {
position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
display: flex; gap: 12px;
}
.screen-nav__dot {
width: 10px; height: 10px; border-radius: 50%;
background: var(--border-card); cursor: pointer; transition: all 0.2s;
}
.screen-nav__dot.is-active {
background: var(--raw-blue);
box-shadow: 0 0 8px var(--border-glow); /* token 同源 */
}
</style>
router-link vs 手写点击:原生版要 addEventListener + 调 router.switchTo;router-link 声明式完成,且自动处理激活态、键盘可达性。
四、与 v-show 时代的本质差异
| 维度 | v-show 三屏(Week 11) | router-view(今天) |
|---|---|---|
| URL | 不变 | /a /b /c 即状态 |
| 刷新 | 回到首屏 | 停留当前屏 ✅ |
| 后退键 | 无效 | 逐屏回退 ✅ |
| 分享 | 不可 | 发链接直达某屏 ✅ |
| 屏幕实例 | 常驻(内存 × 3) | 切走即销毁(Day 82 缓存找回) |
| 代码分割 | 全量加载 | 懒加载按屏分包 ✅ |
工业场景的现实意义:值班室的大屏重启后自动回到"拓扑页"而不是总览页;运维排查时可以直接把 /b 发给同事——URL 成为工作流的一部分。
五、404 与重定向
// 路由表的两条兜底规则(第一节已含,这里解释为什么)
{ path: "/", redirect: "/a" }, // 根路径:用户直接开 IP,落到默认屏
{ path: "/:pathMatch(.*)*", redirect: "/a" }, // 任意未知路径:兜底回屏 A
大屏是封闭演示场景,404 页没有意义——兜底重定向比错误页体验更好。若未来做多用户系统再改 404,记待办。
六、常见坑点
坑 1:v-show 残留导致"两套切换逻辑打架"
ScreenA/B/C 组件内部还有 Week 11 的 v-show 相关 class(screen-hidden 等)。路由接管后这些永远不生效,但会误导调试。清理:全局 CSS 里的 .screen-hidden 与相关过渡一并删除。
坑 2:路由切换后图表 0 尺寸 init
router-view 挂载新屏时,VChart 的双保险(Day 76 的 tryInit + ResizeObserver)自动兜住——昨天封装的基座今天直接兑现价值。若仍失败,检查屏组件根元素的 grid 区域是否声明了高度(grid 布局的 1fr 依赖父级高度)。
坑 3:devtools 里路由不显示
createWebHistory 需要 Vue Devtools 6+。另外确认 app.use(router) 在 mount 之前——顺序反了路由系统未初始化。
坑 4:刷新 404(部署时)
history 模式下刷新 /b,静态服务器找不到对应文件。开发环境 Vite 自动处理;部署时需要回退配置(把所有路径指回 index.html)。今天只记录,Week 14 收官时处理部署。
七、自测挑战
T1 · 路由替换(70 分钟)
完成第一至三节:三屏路由化、指示器切换、懒加载分包(Network 面板验证三屏是三个 chunk)、刷新停留当前屏。手写 ScreenRouter 此刻还有轮播职责,暂不删除(Day 79 完成联动后退役)。
T2 · 行为对齐回归(30 分钟)
跑 Day 69 联调清单的路由部分:快速连切无白屏、数据流五断点正常(数据层没动,应该全绿)。发现数据异常先查是否有人绕过 useScreenStore 私连。
T3 · URL 工作流体验(20 分钟)
录一段 30 秒操作:切到 /b → 刷新(停留)→ 后退(回 /a)→ 直接改地址栏 /c。把"URL 即状态"的体验写进对照表。
八、总结
| 环节 | 要点 |
|---|---|
| 路由表 | 短路径 + name 解耦 + 懒加载分包 |
| router-view | 单挂载点:切屏 = 卸载 + 挂载(Day 82 缓存解决) |
| router-link | 声明式导航 + 自动激活态 |
| URL 即状态 | 刷新停留、后退可用、可分享——工作流改进 |
| 兜底 | 重定向优先于 404(大屏场景) |
路由骨架就位。明天补上轮播调度器与守卫,让手写 ScreenRouter 彻底退役。