【Vue3】day79-router-guards-carousel

作者:mario 发布时间: 2026-09-03 阅读量:5 评论数:0

Day 79 · 路由进阶 — 守卫、过渡与轮播联动:ScreenRouter 退役日

昨天路由表跑通,但轮播还握在手写 ScreenRouter 手里。今天写一个纯决策的轮播调度器(composable):它只回答"何时该切、切到哪",切换动作统一走 router.push——职责分离后,守卫(beforeEach)成为所有切换的"关卡",过渡(Transition)成为所有切换的"皮肤"。今天结束,screen-router.ts 零引用,光荣退役。


目录


一、轮播调度器:只决策,不执行

1.1 原生版的职责纠缠

Day 64 的 ScreenRouter 身兼四职:屏幕注册(数据)+ 切换执行(DOM)+ 轮播计时(策略)+ 事件广播(通信)。四职纠缠的代价:

  • 想改"切屏动画"要动它,想改"停留时长"要动它,想加"守卫拦截"还是得动它
  • 它直接操作 classList——切换的"执行"没有统一关卡,无法拦截

1.2 Vue 版的职责分配

┌─────────────────────────────────────────────┐
│ useCarousel(composable)—— 只做决策          │
│   职责:计时、算下一屏、暂停/恢复              │
│   动作:决策出结果后调 router.push            │
├─────────────────────────────────────────────┤
│ Vue Router —— 只做执行                        │
│   职责:URL 变更、组件挂载切换、守卫关卡        │
├─────────────────────────────────────────────┤
│ Transition —— 只做皮肤                        │
│   职责:切换时的淡入淡出                      │
└─────────────────────────────────────────────┘

分工收益:守卫成为"所有切换的必经之路"——无论是轮播触发、点击圆点、还是用户按后退键,都会被守卫统一审查。这在原生版里做不到(三条路径三个入口)。


二、调度器实现:useCarousel

// src/composables/useCarousel.ts
import { onMounted, onUnmounted, ref } from "vue";
import { useRouter } from "vue-router";
import { SCREENS } from "../components/ScreenNav.vue";   // Day 78 的单一来源

/**
 * 轮播调度器(组合式函数)
 * 迁移自 Day 64 ScreenRouter 的 scheduleNext/dwellMs/setAutoPlay
 *
 * 设计要点:
 * 1. 只决策不执行:到点调 router.push,不碰任何 DOM
 * 2. 手动切换重置计时:用户点了圆点,当前屏的停留从头算(原生版行为)
 * 3. 页面隐藏自动暂停:visibilitychange(Day 64 的省 CPU 策略)
 * 4. 成对纪律:定时器在 onUnmounted 清理
 */
export function useCarousel() {
  const router = useRouter();
  const autoplay = ref(true);
  let timer = 0;

  /** 停留时长查表(SCREENS 单一来源) */
  const dwellOf = (name: string): number =>
    (SCREENS.find((s) => s.name === name)?.dwell ?? 15) * 1000;

  /** 到点切下一屏:按 SCREENS 顺序循环 */
  const next = (): void => {
    const idx = SCREENS.findIndex((s) => s.name === route.name);
    const target = SCREENS[(idx + 1) % SCREENS.length];
    router.push({ name: target.name });
  };

  /** 重排计时:路由每次变化后调用(守卫 afterEach 里挂——见第三节) */
  const reschedule = (): void => {
    clearTimeout(timer);
    if (!autoplay.value) return;
    timer = window.setTimeout(next, dwellOf(String(route.name)));
  };

  /** 暂停/恢复(用户交互、Tab 切换时用) */
  const setAutoplay = (on: boolean): void => {
    autoplay.value = on;
    if (on) reschedule();
    else clearTimeout(timer);
  };

  // 页面隐藏暂停(省 CPU——值班室大屏的常驻场景)
  const onVisibility = (): void => setAutoplay(!document.hidden);

  onMounted(() => document.addEventListener("visibilitychange", onVisibility));
  onUnmounted(() => {
    document.removeEventListener("visibilitychange", onVisibility);
    clearTimeout(timer);
  });

  return { autoplay, setAutoplay };
}

注意 reschedule 需要感知路由变化——正确挂法在守卫的 afterEach(下一节),而不是在调度器里 watch route:让"路由变了要重排计时"成为路由系统的官方副作用,而非调度器的私有联动。


三、路由守卫:所有切换的关卡

// src/router/index.ts(在 Day 78 基础上追加守卫)
import { createRouter, createWebHistory } from "vue-router";
import { useCarousel } from "../composables/useCarousel";

export const router = createRouter({ /* Day 78 路由表原样 */ });

// —— 守卫链:每次切换依次过三关 ——

/**
 * 关 1:beforeEach —— 拦截/放行决策
 * 大屏场景的用武之地:告警确认弹窗未关闭时阻止切屏(Day 82 接入弹窗后启用)
 */
router.beforeEach((to) => {
  // 本周先放行全部;Day 82 在这里挂"告警确认中不许切屏"
  return true;
});

/**
 * 关 2:afterEach —— 切换完成的副作用
 * 轮播计时重排挂在这里:无论是轮播自切、点击、后退键,统一重排
 */
router.afterEach(() => {
  // 惰性获取调度器实例(避免循环依赖:composable import router)
  carousel?.reschedule();
});

// 调度器实例的持有:在 App.vue 里 useCarousel() 创建一次,
// 通过模块级变量注入(见坑 2 的更优解:provide/inject 或直接模块单例)
import { useCarousel } from "../composables/useCarousel";
let carousel: ReturnType<typeof useCarousel> | null = null;
export function bindCarousel(c: ReturnType<typeof useCarousel>): void {
  carousel = c;
}

App.vue 里的接线:

<script setup lang="ts">
import { useCarousel } from "./composables/useCarousel";
import { bindCarousel } from "./router";

// 创建调度器并注入路由层(App 只创建一次)
bindCarousel(useCarousel());
</script>

🎯 对照原生版:Day 64 的 switchTo 里"切屏 + 广播 + 重排计时"是揉在一起的;现在三个关注点分居守卫链、Transition、afterEach——每个改动都有唯一的位置


四、路由过渡:所有切换的皮肤

<!-- App.vue:router-view 套 Transition -->
<template>
  <div class="screen-canvas" :style="{ transform: `scale(${scale})` }">
    <!-- mode="out-in":旧的淡出完再淡入(原生版 opacity 互斥的过渡版) -->
    <router-view v-slot="{ Component }">
      <Transition name="screen" mode="out-in">
        <component :is="Component" />
      </Transition>
    </router-view>
  </div>
</template>

<style>
/* 屏幕切换过渡:与原生版 Day 64 的 opacity 过渡参数一致(视觉零变化) */
.screen-enter-active,
.screen-leave-active { transition: opacity 0.3s ease; }
.screen-enter-from,
.screen-leave-to { opacity: 0; }
</style>

v-slot 写法的必要性<Transition> 要求单一子元素,而 router-view 是动态组件出口——v-slot="{ Component }" 把当前组件取出来包进 Transition,这是官方推荐的组合姿势。


五、退役仪式:删除手写 ScreenRouter

三步走完绞杀者模式:

1. 全局搜索 screen-router 的 import —— 应为零引用(屏组件已走 router-view)
2. 删除 src/core/screen-router.ts
3. npm run build —— 绿了才算退役完成

对照表补四行(本周作业):

场景 原生 ScreenRouter Vue Router + useCarousel
切换执行 classList 操作 router.push + Transition
轮播计时 switchTo 内联重排 afterEach 统一重排
拦截时机 beforeEach 关卡
URL 状态 刷新停留/后退可用

六、常见坑点

坑 1:调度器里 watch route 重排计时

路由变化 → watch 触发重排。功能上可行,但后退键、手动点击、轮播自切三条路径的时序差异会带来边缘 bug(watch 是异步批处理的)。规范挂法:afterEach(同步、必经、语义明确)。

坑 2:循环依赖——composable import router,router 又 import composable

Day 79 的 bindCarousel 就是为此而生(路由模块不 import composable,靠注入)。替代方案:调度器挪到独立模块、只 import router 单例。任何方向都行,禁止双向 import

坑 3:Transition 与 Day 67 入场动效打架

屏组件内部卡片还有 card-enter 入场动画(Day 67 迁移版),与屏级 screen 过渡叠加出现"双重渐入"(Day 69 坑 1 的路由版)。规则:屏级过渡管屏、卡片入场管内容,卡片入场只在首次挂载播(KeepAlive 后只在首次激活播——明天展开)。

坑 4:mode=“out-in” 切换变慢

淡出 0.3s + 淡入 0.3s = 0.6s 的"换屏空窗"。若嫌慢改 mode 为默认(同时进出)并接受短暂叠加,或缩短 leave 到 0.15s。大屏演示节奏 0.3/0.3 是安全值,改动后要观察与轮播 dwell 的观感配合。


七、自测挑战

T1 · 调度器 + 守卫 + 过渡(80 分钟)

完成第二至四节:轮播按 dwell 自动循环(A 15s → B 20s → C 15s)、点击圆点后当前屏计时重置、后退键触发过渡且轮播重排、Tab 切走暂停/切回恢复。

T2 · 退役验证(20 分钟)

删除 screen-router.ts,build 绿;跑 Day 69 联调清单路由部分(快速连切、数据链路断点)。行为零变化(URL 能力除外)是替换成功的判据。

T3 · 守卫演练(20 分钟)

在 beforeEach 里临时加"5 秒内只许切一次屏"的节流守卫,体验拦截:快速连点圆点时部分切换被拦下(Console 打印拦截日志)。理解后删除——这是为 Day 82 告警拦截做的彩排。


八、总结

环节 要点
职责分离 调度器只决策、路由只执行、Transition 只管皮肤
守卫链 beforeEach 拦截 / afterEach 重排——所有切换必经
过渡 v-slot + out-in,参数对齐原生版
退役判据 零 import + build 绿 + 行为零变化
对照表 四行入账,本周任务过半

路由层替换完毕。明天开始第二刀:Pinia 接管全局状态。

评论