程序化路由与深链接设计

不是所有导航都通过 <NuxtLink> 完成——表单提交后跳转、权限校验后重定向、条件性导航都需要程序化路由控制。本章详解 useRouter/useRoute 完整 API、路由历史栈管理、Query 参数设计模式,以及 AI 视频播放页的深链接实战方案。

1. useRouter / useRoute 完整 API

1.1 useRoute — 当前路由信息

useRoute() 返回当前路由的只读响应式对象:

<script setup lang="ts">
const route = useRoute()
 
// 常用属性
route.path         // '/videos/123'        — URL 路径
route.fullPath     // '/videos/123?t=30'   — 完整路径(含 query 和 hash)
route.params       // { id: '123' }        — 动态参数
route.query        // { t: '30' }          — 查询参数
route.hash         // '#comments'          — 哈希值
route.name         // 'videos-id'          — 路由名称
route.meta         // { ... }              — definePageMeta 中的自定义字段
route.matched      // RouteRecord[]        — 匹配的路由记录链
route.redirectedFrom // RouteLocation      — 重定向来源
</script>

1.2 useRouter — 路由实例

useRouter() 返回路由实例,用于程序化导航

const router = useRouter()
 
// ── 导航方法 ──
router.push('/videos')                    // 添加历史记录
router.push({ name: 'videos-id', params: { id: '123' } })
router.replace('/videos')                 // 替换当前历史记录(无后退)
router.back()                             // 后退
router.forward()                          // 前进
router.go(-2)                             // 后退 2 步
 
// ── 路由信息 ──
router.currentRoute.value                 // 当前路由(同 useRoute())
router.getRoutes()                        // 获取所有已注册路由
router.hasRoute('videos-id')              // 检查路由是否存在
router.resolve({ name: 'videos-id', params: { id: '123' } })  // 解析路由
 
// ── 导航守卫 ──
router.beforeEach((to, from) => { ... })  // 全局前置守卫
router.afterEach((to, from) => { ... })   // 全局后置守卫
router.onError((error) => { ... })        // 导航错误处理

1.3 navigateTo vs router.push

Nuxt4 中推荐使用 navigateTo 而不是 router.push

特性navigateTorouter.push
SSR 支持✅ 服务端发送 302 重定向❌ 仅客户端
中间件中可用❌(在中间件上下文中不可靠)
外部链接&#123; external: true &#125;
HTTP 状态码&#123; redirectCode: 301 &#125;
通用性✅ 两端通用仅客户端
// ✅ 推荐:在所有 Nuxt 上下文中都安全
navigateTo('/videos')
navigateTo({ name: 'videos-id', params: { id: '123' } })
 
// ⚠️ 仅在确定是客户端环境时使用
const router = useRouter()
router.push('/videos')

1.4 路由守卫的最佳实践

// app/plugins/router-guard.ts
export default defineNuxtPlugin(() => {
  const router = useRouter()
 
  // 全局后置守卫:页面访问日志
  router.afterEach((to, from) => {
    if (import.meta.client) {
      console.log(`[Navigation] ${from.path} → ${to.path}`)
    }
  })
 
  // 全局错误处理
  router.onError((error, to, from) => {
    console.error(`[Navigation Error] ${from.path} → ${to.path}`, error)
 
    // 网络错误时显示提示
    if (error.message.includes('Failed to fetch')) {
      // 显示离线提示或重试按钮
    }
  })
})

2. 路由历史栈管理

2.1 push vs replace

// push:添加新记录到历史栈
// 用户可以点击后退回到上一页
navigateTo('/videos/123')
 
// replace:替换当前记录
// 用户点击后退会跳过当前页
navigateTo('/videos/123', { replace: true })

何时用 replace?

// 场景 1:登录后跳转(不希望后退回到登录页)
async function handleLogin() {
  await login(credentials)
  navigateTo('/', { replace: true })
}
 
// 场景 2:URL 规范化(如 /Videos → /videos)
// 不产生额外的历史记录
navigateTo('/videos', { replace: true, redirectCode: 301 })
 
// 场景 3:表单提交后跳转
async function handleSubmit() {
  await $fetch('/api/videos', { method: 'POST', body: formData })
  navigateTo('/studio/projects', { replace: true })
}

2.2 后退与前进

const router = useRouter()
 
// 后退一步
router.back()
 
// 前进一步
router.forward()
 
// 后退 N 步
router.go(-2)
 
// 自定义返回逻辑
function goBack() {
  // 如果有历史记录,后退;否则回到首页
  if (window.history.length > 1) {
    router.back()
  } else {
    navigateTo('/')
  }
}

2.3 导航完成检测

navigateTorouter.push 都返回 Promise:

// 等待导航完成后执行操作
await navigateTo('/videos')
console.log('导航完成!')
 
// 检测导航是否成功
try {
  await router.push('/videos')
} catch (error) {
  if (isNavigationFailure(error, NavigationFailureType.aborted)) {
    console.log('导航被中间件中止')
  }
  if (isNavigationFailure(error, NavigationFailureType.duplicated)) {
    console.log('已经在目标页面上了')
  }
}

3. Query 参数设计模式

3.1 基本用法

<script setup lang="ts">
const route = useRoute()
 
// 读取 query 参数
const page = computed(() => Number(route.query.page) || 1)
const sort = computed(() => (route.query.sort as string) || 'newest')
const search = computed(() => (route.query.q as string) || '')
</script>

3.2 更新 Query 参数

const router = useRouter()
const route = useRoute()
 
// 更新单个参数(保留其他参数)
function updateQuery(key: string, value: string | undefined) {
  const query = { ...route.query }
  if (value) {
    query[key] = value
  } else {
    delete query[key]
  }
  router.push({ query })
}
 
// 使用示例
updateQuery('page', '2')          // ?...&page=2
updateQuery('sort', 'popular')    // ?...&sort=popular
updateQuery('page', undefined)    // 移除 page 参数

3.3 useRouteQuery Composable

封装一个通用的 Query 参数 Composable:

// app/composables/useRouteQuery.ts
export function useRouteQuery(key: string, defaultValue = '') {
  const route = useRoute()
  const router = useRouter()
 
  const value = computed({
    get: () => (route.query[key] as string) || defaultValue,
    set: (newValue) => {
      const query = { ...route.query }
      if (newValue && newValue !== defaultValue) {
        query[key] = newValue
      } else {
        delete query[key]
      }
      router.replace({ query })
    },
  })
 
  return value
}

使用时就像普通的 ref:

<script setup lang="ts">
const page = useRouteQuery('page', '1')
const sort = useRouteQuery('sort', 'newest')
 
// 读取
console.log(page.value)  // '1'
 
// 修改(自动更新 URL)
page.value = '2'  // URL 变为 ?page=2&sort=newest
</script>

4. AI 视频播放页深链接设计

4.1 什么是深链接

深链接(Deep Link)是指 URL 不仅指向一个页面,还精确描述页面内的状态——播放时间、选中的字幕轨、画质、是否全屏等。用户可以分享 URL 让他人看到完全相同的页面状态。

4.2 URL 结构设计

/videos/abc123?t=125&quality=1080p&subtitle=cn&autoplay=1#comments

路径参数:
  abc123        — 视频 ID

Query 参数:
  t=125         — 播放到第 125 秒
  quality=1080p — 视频画质
  subtitle=cn   — 字幕语言
  autoplay=1    — 自动播放

Hash:
  #comments     — 滚动到评论区

4.3 完整实现

// app/composables/useVideoDeepLink.ts
interface VideoDeepLinkState {
  time: Ref<number>
  quality: Ref<string>
  subtitle: Ref<string>
  autoplay: Ref<boolean>
}
 
export function useVideoDeepLink(): VideoDeepLinkState {
  const route = useRoute()
  const router = useRouter()
 
  // 从 URL 读取初始状态
  const time = ref(Number(route.query.t) || 0)
  const quality = ref((route.query.quality as string) || '1080p')
  const subtitle = ref((route.query.subtitle as string) || '')
  const autoplay = ref(route.query.autoplay === '1')
 
  // 同步状态到 URL(防抖避免频繁更新)
  let updateTimer: ReturnType<typeof setTimeout>
 
  function syncToUrl() {
    clearTimeout(updateTimer)
    updateTimer = setTimeout(() => {
      const query: Record<string, string> = {}
      if (time.value > 0) query.t = String(Math.floor(time.value))
      if (quality.value !== '1080p') query.quality = quality.value
      if (subtitle.value) query.subtitle = subtitle.value
      if (autoplay.value) query.autoplay = '1'
 
      router.replace({ query })
    }, 1000)
  }
 
  // 监听状态变化,自动同步到 URL
  watch([time, quality, subtitle, autoplay], syncToUrl)
 
  return { time, quality, subtitle, autoplay }
}
<!-- app/pages/videos/[id].vue -->
<script setup lang="ts">
const route = useRoute()
const { data: video } = await useFetch(`/api/videos/${route.params.id}`)
 
const { time, quality, subtitle, autoplay } = useVideoDeepLink()
 
// 播放器时间更新时同步
function onTimeUpdate(currentTime: number) {
  time.value = currentTime
}
 
// 分享当前播放位置
function shareCurrentPosition() {
  const url = new URL(window.location.href)
  url.searchParams.set('t', String(Math.floor(time.value)))
  navigator.clipboard.writeText(url.toString())
}
</script>
 
<template>
  <div v-if="video">
    <VideoPlayer
      :url="video.url"
      :initial-time="time"
      :quality="quality"
      :subtitle="subtitle"
      :autoplay="autoplay"
      @time-update="onTimeUpdate"
      @quality-change="(q) => quality = q"
      @subtitle-change="(s) => subtitle = s"
    />
 
    <div class="mt-4 flex items-center gap-4">
      <h1 class="text-2xl font-bold flex-1">{{ video.title }}</h1>
      <button @click="shareCurrentPosition" class="btn-secondary">
        分享当前位置
      </button>
    </div>
  </div>
</template>

4.4 Hash 导航:滚动到评论区

<script setup lang="ts">
const route = useRoute()
 
// 页面加载时,如果 URL 有 #comments,滚动到评论区
onMounted(() => {
  if (route.hash === '#comments') {
    nextTick(() => {
      document.getElementById('comments')?.scrollIntoView({ behavior: 'smooth' })
    })
  }
})
</script>
 
<template>
  <div>
    <VideoPlayer ... />
    <VideoInfo ... />
 
    <!-- 评论区锚点 -->
    <section id="comments" class="mt-8">
      <h2 class="text-xl font-bold">评论</h2>
      <CommentList :video-id="video.id" />
    </section>
  </div>
</template>

4.5 scrollBehavior 配合

// app/router.options.ts
import type { RouterConfig } from '@nuxt/schema'
 
export default <RouterConfig>{
  scrollBehavior(to, from, savedPosition) {
    // 浏览器后退/前进:恢复滚动位置
    if (savedPosition) return savedPosition
 
    // 带 hash:滚动到锚点
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth',
        top: 80,  // 留出固定导航栏的高度
      }
    }
 
    // 同一页面内的 query 变化:不滚动
    if (to.path === from.path) return false
 
    // 其他情况:滚动到顶部
    return { top: 0, behavior: 'smooth' }
  },
}

本章小结

  • useRoute:只读响应式对象,获取 path/params/query/hash/meta
  • useRouter:路由实例,提供 push/replace/back/go 等导航方法
  • navigateTo vs router.push:前者 SSR 通用,推荐优先使用
  • 历史栈管理:push 添加记录、replace 替换记录,登录/表单提交后用 replace
  • Query 参数:封装 useRouteQuery 实现 URL↔状态的双向绑定
  • 深链接:URL 精确描述页面状态(播放时间/画质/字幕),支持分享和恢复
  • scrollBehavior:自定义滚动行为,配合 hash 锚点和历史记录恢复

至此,第二篇"路由系统精讲"全部 5 章已完成。你已经掌握了:

  1. 文件系统路由的映射规则与 NuxtPage 工作原理
  2. 动态路由、嵌套路由、路由分组的高级模式
  3. 路由中间件体系与权限拦截完整方案
  4. CSS 过渡动画与 View Transitions API 共享元素动画
  5. 程序化路由控制与深链接设计

下一篇将深入 组件系统精讲——组件自动导入、Props/Emits/Slots 高级用法、NuxtUI 集成。