Vue.js从零到精通系列(四):前端路由与Vue Router——打造多页单页应用

摘要: 从本篇开始,我们的待办应用将摆脱"单页面"的束缚,正式进化为拥有多个视图、可前进后退的现代单页应用(SPA)。前端路由正是实现这一切的核心机制。本文将先为你揭开路由的神秘面纱,从 Hash 和 History 两种模式的工作原理讲起,然后逐步带你安装 Vue Router、定义路由表、使用 <router-link><router-view> 完成页面跳转。接着,我们将深入动态路由、嵌套路由、命名视图、编程式导航,以及至关重要的路由守卫和元信息。配合 TypeScript 的类型增强,你会学会如何构建安全、可维护的应用导航体系。最终,我们会将之前的 Todo 应用拆分为列表页、详情页和设置页,用路由串联它们,真正感受 SPA 的流畅体验。


一、前端路由是什么?为什么要用路由?

1.1 从"多页"到"单页"的进化

还记得你初学 HTML 时所做的网页吗?点击一个链接,浏览器向服务器请求新的 HTML 文件,整个页面重新加载------这就是多页应用(MPA)。这种模式在用户体验上存在明显割裂感:每次跳转都有白屏,状态也难以保持。

后来,Ajax 技术让局部更新成为可能,但 URL 并不会变化,用户无法通过浏览器的"前进/后退"回到之前的状态,也无法把当前视图加入书签。前端路由 应运而生:它让 URL 变化时不向服务器发送请求,而是由 JavaScript 接管,动态替换页面的部分内容。这便是**单页应用(SPA)**的基础。

1.2 两种路由模式:Hash vs History

Vue Router 支持两种模式,但它们的原理你最好心知肚明。

  • Hash 模式 URL 中带有一个 # 号,例如 http://example.com/#/list# 及其后面的部分被称为哈希(hash)。浏览器不会把 # 后的内容发送给服务器,它的变化不会触发页面刷新,但会被记录在浏览历史中。Vue Router 默认使用 hash 模式,因为它兼容所有浏览器,部署也最省心。

  • History 模式 利用 HTML5 的 history.pushStatehistory.replaceState API,可以实现完全不带 # 的漂亮 URL,如 http://example.com/list。这种模式对 SEO 更友好,用户也看着更自然。但它需要后端配合:因为刷新页面时,浏览器会向服务器请求这个路径,服务器必须始终返回 index.html,否则会出现 404。

Vue Router 3.x 默认使用 hash,4.x(对应 Vue 3)也支持通过 createWebHistory 切换到 history 模式。对于你的项目,初期可直接使用 hash 模式,后续上线再配置 nginx 等后端支持。


二、Vue Router 初体验

2.1 创建 Vite + Vue3 项目

TypeScript 复制代码
pnpm create vite my-vue-router-app --template vue-ts
cd my-vue-router-app
pnpm install

2.2 安装与创建路由

在项目中安装 vue-router(注意,Vue 3 必须使用 4.x 版本):

TypeScript 复制代码
pnpm add vue-router@4

接着创建路由文件,一般放在 src/router/index.ts

TypeScript 复制代码
import { createRouter, createWebHashHistory } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/HomePage.vue'),
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutPage.vue'),
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('../views/NotFound.vue'),
  }
]
const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

export default router

这里使用了动态导入 () => import(...),Vite 会将其自动拆分为独立的 chunk,实现路由级别的懒加载,首屏更快。

2.3 在 Vue 应用中安装路由

修改 src/main.ts,通过 app.use(router) 注册:

TypeScript 复制代码
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router' // 引入路由

const app = createApp(App)

app.use(router) // 注册路由

app.mount('#app')

app.use() 你之前可能用过,它是 Vue 插件系统的标准入口。Vue Router 作为插件被安装后,整个应用都可以访问路由实例。

2.4 放置路由出口:<router-view>

现在修改 App.vue,删除之前的内容,换成 <router-view>

TypeScript 复制代码
<template>
  <div id="app">
    <!-- 导航栏 -->
    <nav class="nav">
      <router-link to="/">首页</router-link>
      <router-link to="/about">关于</router-link>
    </nav>

    <!-- 路由页面渲染在这里 -->
    <div class="container">
      <router-view />
    </div>
  </div>
</template>

<style scoped>
.nav {
  padding: 16px;
  background: #f5f5f5;
  margin-bottom: 20px;
}
.nav a {
  margin-right: 16px;
  text-decoration: none;
  color: #333;
}
.nav a:hover {
  color: #0066cc;
}
.container {
  padding: 0 16px;
}
</style>

<router-view> 是一个动态组件,会根据当前 URL 渲染匹配的页面组件。<router-link> 则会被渲染为一个 <a> 标签,点击时不会刷新页面,只触发前端路由切换。

2.4 编写简单页面

src/views/ 下创建三个文件:

src/views/HomePage.vue(首页)

TypeScript 复制代码
<template>
  <div>
    <h1>🏠 首页</h1>
    <p>欢迎使用 Vue 3 + Vue Router 4</p>
  </div>
</template>

src/views/AboutPage.vue(关于页)

TypeScript 复制代码
<template>
  <div>
    <h1>ℹ️ 关于页面</h1>
    <p>这是一个单页应用(SPA)</p>
  </div>
</template>

src/views/NotFound.vue(404 页面)

TypeScript 复制代码
<template>
  <div>
    <h1>❌ 404 页面不存在</h1>
    <p><router-link to="/">返回首页</router-link></p>
  </div>
</template>

运行 pnpm dev,点击导航链接,页面内容无刷新切换,URL 也会自动变成 /#//#/about。你的第一个 SPA 已经诞生!


三、动态路由:路径中的变量

真实的业务中,路由往往携带变量。比如商品详情页需要商品 ID,/product/123。Vue Router 的动态路由: 定义参数。

3.1 定义动态路由

在src/router/index.ts文件中添加:

TypeScript 复制代码
{
    path: '/todo/:id',
    name: 'todo-detail',
    component: () => import('../views/TodoDetail.vue')
}

:id 表示动态部分,可以匹配任意字符串(不包括 /)。如果希望参数可选,可以在后面加 ?/:id?

3.2 获取并监听路由参数

创建【待办详情页】src/views/TodoDetail.vue,在组件中,通过 useRoute 获取参数:

TypeScript 复制代码
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { watch } from 'vue'

// 1. 获取当前路由
const route = useRoute()

// 2. 拿到动态参数 id
const todoId = route.params.id

// 3. 监听参数变化(从 /todo/1 → /todo/2 会触发)
watch(
  () => route.params.id,
  (newId) => {
    console.log('ID 变了:', newId)
    // 这里可以重新请求接口拿数据
  }
)
</script>

<template>
  <div>
    <h1>待办详情</h1>
    <h3>当前路由参数:{{ todoId }}</h3>
    <p>你正在查看第 {{ todoId }} 号任务</p>
    <br />
    <router-link to="/">← 返回首页</router-link>
  </div>
</template>

如果你想让 TypeScript 知道 id 一定是字符串(因为路径是 :id),可以利用路由的类型声明进行增强。Vue Router 提供了类型扩展接口,但最简单的方式就是使用非空断言或类型转换。

3.3 使用 useRouteuseRouter 的区别

  • useRoute() 返回当前路由的响应式对象,包含 paramsqueryhashmeta 等,用于读取信息。

  • useRouter() 返回路由器实例,用于编程式导航(如 pushreplace)。

TypeScript 复制代码
import { useRouter, useRoute } from 'vue-router'
​
const router = useRouter()
const route = useRoute()
​
function goToDetail(id: number) {
  router.push({ name: 'todo-detail', params: { id } })
}

3.4 修改主页文件

src/views/HomePage.vue

TypeScript 复制代码
<script setup lang="ts">
import { useRouter } from 'vue-router'

// 获取路由实例(用来跳转)
const router = useRouter()

// 编程式导航(点击按钮跳转)
function goDetail(id: number) {
  router.push({
    name: 'todo-detail',
    params: { id: id }
  })
}
</script>

<template>
  <div>
    <h1>首页 → 待办列表</h1>

    <!-- 方式1:router-link 跳转 -->
    <div>
      <p>方式1:标签跳转</p>
      <router-link to="/todo/1">查看 1 号任务</router-link>
      <br />
      <router-link to="/todo/2">查看 2 号任务</router-link>
    </div>

    <br />
    <!-- 方式2:点击事件跳转 -->
    <div>
      <p>方式2:按钮编程式跳转</p>
      <button @click="goDetail(100)">查看 100 号任务</button>
      <button @click="goDetail(200)">查看 200 号任务</button>
    </div>
  </div>
</template>

3.5 App.vue 加导航

修改src/App.vue

TypeScript 复制代码
<template>
  <div>
    <nav>
      <router-link to="/">首页</router-link> |
      <router-link to="/about">关于</router-link> |
      <router-link to="/todo/1">任务1</router-link> |
      <router-link to="/todo/66">任务66</router-link>
    </nav>
    <hr />
    <router-view />
  </div>
</template>

四、嵌套路由与命名视图

4.1 嵌套路由

当页面有公共布局(如侧边栏、顶栏)时,我们希望子页面嵌套在布局内部切换。Vue Router 通过 children 配置实现:

TypeScript 复制代码
const routes: RouteRecordRaw[] = [
  {
    path: '/user',
    component: () => import('../views/UserLayout.vue'),
    children: [
      {
        path: '',                 // 默认子路由
        component: () => import('../views/UserProfile.vue')
      },
      {
        path: 'settings',
        component: () => import('../views/UserSettings.vue')
      }
    ]
  }
]

4.2 命名视图

有时候,一个页面需要同时展示多个同级组件(比如侧边栏和主内容同时变化)。命名视图可以使用 components 替代 component,并给 <router-view> 指定 name

TypeScript 复制代码
{
  path: '/dashboard',
  components: {
    default: () => import('../views/DashboardMain.vue'),
    sidebar: () => import('../views/DashboardSidebar.vue')
  }
}

整体修改src/router/index.ts如下:

TypeScript 复制代码
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  // 基础路由
  {
    path: '/',
    name: 'home',
    component: () => import('../views/HomePage.vue')
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutPage.vue')
  },

  // 动态路由(承接上一节)
  {
    path: '/todo/:id',
    name: 'todo-detail',
    component: () => import('../views/TodoDetail.vue')
  },

  // ========== 4.1 嵌套路由 ==========
  {
    path: '/user',
    name: 'user',
    component: () => import('../views/UserLayout.vue'),
    // 子路由数组
    children: [
      // 默认子路由:访问 /user 时渲染
      {
        path: '',
        name: 'user-profile',
        component: () => import('../views/UserProfile.vue')
      },
      // 子路由:访问 /user/settings
      {
        path: 'settings',
        name: 'user-settings',
        component: () => import('../views/UserSettings.vue')
      }
    ]
  },

  // ========== 4.2 命名视图 ==========
  {
    path: '/dashboard',
    name: 'dashboard',
    // 命名视图使用 components (复数)
    components: {
      default: () => import('../views/DashboardMain.vue'),
      sidebar: () => import('../views/DashboardSidebar.vue')
    }
  },

  // 404 兜底路由
  {
    path: '/:pathMatch(.*)*',
    name: 'not-found',
    component: () => import('../views/NotFound.vue')
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

4.3 公共布局 src/views/UserLayout.vue

TypeScript 复制代码
<template>
  <div class="user-layout">
    <!-- 侧边导航 -->
    <aside class="aside">
      <h3>用户中心</h3>
      <div class="nav-item">
        <router-link to="/user">个人资料</router-link>
      </div>
      <div class="nav-item">
        <router-link to="/user/settings">账户设置</router-link>
      </div>
    </aside>

    <!-- 子页面渲染出口 -->
    <main class="main">
      <router-view />
    </main>
  </div>
</template>

<style scoped>
.user-layout {
  display: flex;
  gap: 20px;
  margin-top: 20px;
}
.aside {
  width: 180px;
  padding: 16px;
  background: #f5f5f5;
}
.nav-item {
  margin: 10px 0;
}
a {
  text-decoration: none;
  color: #333;
}
a.router-link-active {
  color: #42b983;
  font-weight: bold;
}
.main {
  flex: 1;
  padding: 16px;
  border: 1px solid #eee;
}
</style>

4.4 默认子页面 src/views/UserProfile.vue

TypeScript 复制代码
<template>
  <div>
    <h2>个人资料</h2>
    <p>这是 /user 路由对应的默认子页面</p>
  </div>
</template>

4.5 设置页面 src/views/UserSettings.vue

TypeScript 复制代码
<template>
  <div>
    <h2>账户设置</h2>
    <p>这是 /user/settings 子页面</p>
  </div>
</template>

4.6 侧边栏视图 src/views/DashboardSidebar.vue

TypeScript 复制代码
<template>
  <div class="sidebar">
    <h3>控制台侧边栏</h3>
    <p>菜单1</p>
    <p>菜单2</p>
    <p>菜单3</p>
  </div>
</template>

<style scoped>
.sidebar {
  width: 160px;
  padding: 16px;
  background: #eef2f7;
}
</style>

4.7 主内容视图 src/views/DashboardMain.vue

TypeScript 复制代码
<template>
  <div class="main-content">
    <h2>控制台主内容</h2>
    <p>命名视图 - 默认视图区域</p>
  </div>
</template>

<style scoped>
.main-content {
  flex: 1;
  padding: 16px;
  border: 1px solid #ccc;
}
</style>

4.8 修改根组件 App.vue

TypeScript 复制代码
<template>
  <div class="app">
    <!-- 全局导航 -->
    <nav class="global-nav">
      <router-link to="/">首页</router-link>
      <router-link to="/about">关于</router-link>
      <router-link to="/todo/888">动态路由</router-link>
      <router-link to="/user">用户中心(嵌套路由)</router-link>
      <router-link to="/dashboard">控制台(命名视图)</router-link>
    </nav>

    <hr>

    <!-- 命名视图容器:两个带 name 的 router-view -->
    <div class="dashboard-box">
      <router-view name="sidebar" />
      <router-view /> <!-- 不写name,对应 default 视图 -->
    </div>
  </div>
</template>

<style scoped>
.global-nav {
  margin: 10px 0;
}
.global-nav a {
  margin-right: 15px;
  text-decoration: none;
}
.global-nav a.router-link-active {
  color: #42b983;
  font-weight: bold;
}
.dashboard-box {
  display: flex;
  gap: 20px;
  margin-top: 20px;
}
</style>

五、导航方式与路由传参

5.1 声明式导航:<router-link>

它比硬编码的 <a> 更智能,会自动添加激活时的 CSS 类 router-link-activerouter-link-exact-active,方便高亮当前导航。

TypeScript 复制代码
<router-link to="/" exact-active-class="exact-active">首页</router-link>

5.2 编程式导航

在 JavaScript 中跳转:

TypeScript 复制代码
import { useRouter } from 'vue-router'
const router = useRouter()
​
// 字符串
router.push('/about')
​
// 命名路由 + params
router.push({ name: 'todo-detail', params: { id: 123 } })
​
// 带 query
router.push({ path: '/search', query: { q: 'vue' } })

push 会添加一条历史记录,replace 则会替换当前记录(不会留下记录)。还有 router.go(n) 可以前进或后退 n 步。

5.3 路由传参的方式与选择

除了 params,还可以使用 query。它们的区别:

  • params 属于路径的一部分,在动态路由中定义(:id),刷新页面不会丢失。

  • query? 后面的键值对,不需要预先在路由声明中配置,通常用于可选筛选条件。

TypeScript 提示 :当使用命名路由跳转时,Vue Router 4 提供了有限的类型推断。可以借助 unplugin-vue-router 等增强类型,但对于本篇学习,强类型断言已经足够。


六、路由守卫:把控跳转的每一关

导航守卫让你能在路由切换的各个阶段拦截或重定向。它就像一道安检门,你可以定义"谁可以进、谁需要先登录"。

6.1 全局守卫

router/index.ts 中定义,作用于所有路由:

TypeScript 复制代码
const router = createRouter({ ... })

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 根据条件放行或跳转
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next({ name: 'login', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

// 全局解析守卫(导航被确认之前,所有组件内守卫和异步路由组件被解析之后)
router.beforeResolve((to, from, next) => {
  // 可用于加载数据
  next()
})

// 全局后置钩子(不会改变导航本身)
router.afterEach((to, from) => {
  document.title = to.meta.title as string || '我的应用'
})

Vue Router 4 的守卫支持返回一个值或 Promise 来取代 next 回调(但仍支持 next):

TypeScript 复制代码
router.beforeEach(async (to, from) => {
  if (to.meta.requiresAuth && !(await checkAuth())) {
    return { name: 'login' }
  }
})

6.2 路由独享守卫

可以直接在路由记录上定义 beforeEnter

TypeScript 复制代码
const routes = [
  {
    path: '/admin',
    component: AdminPage,
    beforeEnter: (to, from) => {
      // 只在进入此路由时触发
      if (!isAdmin()) return { name: 'forbidden' }
    }
  }
]

6.3 组件内守卫

在组件内部,你可以通过 onBeforeRouteUpdateonBeforeRouteLeave 钩子实现组件级守卫:

TypeScript 复制代码
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
​
onBeforeRouteUpdate((to, from) => {
  // 当前路由改变但组件复用时调用(如 /todo/1 → /todo/2)
  if (hasUnsavedChanges.value) {
    const answer = window.confirm('有未保存更改,确定离开吗?')
    if (!answer) return false
  }
})
​
onBeforeRouteLeave((to, from) => {
  // 导航离开时调用
  window.clearInterval(timer) // 清理
})
</script>

6.4 完整的导航解析流程

了解生命周期有助于调试:

编辑


七、路由元信息与类型扩展

7.1 使用 meta 存储附加信息

每个路由都可以携带 meta 对象,用于存储权限、图标、标题等自定义数据:

TypeScript 复制代码
const routes: RouteRecordRaw[] = [
  {
    path: '/admin',
    component: AdminPage,
    meta: { requiresAuth: true, role: 'admin', title: '管理后台' }
  }
]

在守卫中读取:

TypeScript 复制代码
router.beforeEach((to) => {
  if (to.meta.requiresAuth) { ... }
})

7.2 为 meta 扩展 TypeScript 类型

默认情况下 route.metaany 类型。为了获得智能提示,可以扩展 RouteMeta 接口:

TypeScript 复制代码
// src/types/router.d.ts
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    title?: string
    role?: string
  }
}

现在在守卫或组件中访问 to.meta.title,TypeScript 会知道你声明的所有字段,极大提升开发体验。


八、综合案例:Todo 应用的多页面改造

让我们把第二篇和第三篇的 Todo 应用正式进化为 SPA,拥有三个页面:列表页、详情页、设置页。

TypeScript 复制代码
src/
├── router
│   └── index.ts          # 路由配置
├── types.ts              # TS类型定义
├── App.vue               # 根布局+导航+路由出口
├── views
│   ├── TodoListPage.vue  # 待办列表页
│   ├── TodoDetailPage.vue# 单条详情页
│   └── SettingsPage.vue  # 设置页面
└── components
    ├── TodoHeader.vue
    ├── TodoInput.vue
    ├── TodoList.vue
    ├── TodoItem.vue
    └── TodoFooter.vue

8.1 路由表设计

src/router/index.ts

TypeScript 复制代码
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'todo-list',
    component: () => import('../views/TodoListPage.vue'),
    meta: { title: '待办列表' }
  },
  {
    path: '/todo/:id',
    name: 'todo-detail',
    component: () => import('../views/TodoDetailPage.vue'),
    meta: { title: '待办详情' }
  },
  {
    path: '/settings',
    name: 'settings',
    component: () => import('../views/SettingsPage.vue'),
    meta: { title: '系统设置' }
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// 全局后置钩子修改浏览器标题
router.afterEach((to) => {
  document.title = (to.meta.title as string) || 'Vue3 Todo SPA应用'
})

export default router
复制代码

8.2 列表页:TodoListPage.vue

我们将原先的 App.vue 中的主要逻辑搬迁到列表页,保留 TodoHeaderTodoInputTodoListTodoFooter 组件。同时增加点击项目跳转到详情页的功能。

TypeScript 复制代码
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import TodoHeader from '../components/TodoHeader.vue'
import TodoInput from '../components/TodoInput.vue'
import TodoList from '../components/TodoList.vue'
import TodoItem from '../components/TodoItem.vue'
import TodoFooter from '../components/TodoFooter.vue'
import type { Todo } from '../types'
​
const router = useRouter()
const todos = ref<Todo[]>([])
let nextId = 1
​
const activeCount = computed(() => todos.value.filter(t => !t.done).length)
const allDone = computed({
  get: () => todos.value.length > 0 && activeCount.value === 0,
  set: (val: boolean) => todos.value.forEach(t => t.done = val)
})
​
function addTodo(text: string) {
  todos.value.push({ id: nextId++, text, done: false })
}
function toggleTodo(id: number) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.done = !todo.done
}
function removeTodo(id: number) {
  todos.value = todos.value.filter(t => t.id !== id)
}
function clearCompleted() {
  todos.value = todos.value.filter(t => !t.done)
}
function goToDetail(id: number) {
  router.push({ name: 'todo-detail', params: { id } })
}
​
onMounted(() => {
  const saved = localStorage.getItem('vue3-todos')
  if (saved) {
    try {
      const data = JSON.parse(saved) as Todo[]
      todos.value = data
      nextId = data.reduce((max, t) => Math.max(max, t.id), 0) + 1
    } catch {}
  }
})
</script>
​
<template>
  <div class="todo-page">
    <TodoHeader :active-count="activeCount" :total="todos.length" />
    <TodoInput @add="addTodo" />
    <TodoList :todos="todos" @toggle="toggleTodo" @remove="removeTodo">
      <template #item="{ todo }">
        <TodoItem :todo="todo" @toggle="toggleTodo" @remove="removeTodo"
          @click="goToDetail(todo.id)" />
      </template>
    </TodoList>
    <TodoFooter v-if="todos.length > 0" v-model="allDone" @clear-completed="clearCompleted" />
  </div>
</template>

注意我们给 TodoItem 添加了点击事件,跳转到详情页。

8.3 详情页:TodoDetailPage.vue

从路由获取 ID,并读取数据展示。真实项目中会从 API 或状态管理获取,这里简单从 localStorage 查找。

TypeScript 复制代码
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type { Todo } from '../types'
const route = useRoute()
const router = useRouter()
const todo = ref<Todo | null>(null)
onMounted(() => {
  const id = Number(route.params.id)
  const saved = localStorage.getItem('vue3-todos')
  if (saved) {
    const todos = JSON.parse(saved) as Todo[]
    todo.value = todos.find(t => t.id === id) || null
  }
})
function goBack() {
  router.push({ name: 'todo-list' })
}
</script>
​
<template>
  <div>
    <button @click="goBack">← 返回列表</button>
    <div v-if="todo">
      <h1>{{ todo.text }}</h1>
      <p>状态:{{ todo.done ? '已完成' : '未完成' }}</p>
      <p>ID:{{ todo.id }}</p>
    </div>
    <div v-else>
      <p>待办不存在</p>
    </div>
  </div>
</template>

8.4 导航栏 App.vue

App.vue 现在只需提供公共导航和路由出口:

复制代码
<template>
  <div id="app">
    <nav class="app-nav">
      <router-link to="/">列表</router-link>
      <router-link to="/settings">设置</router-link>
    </nav>
    <main class="app-main">
      <router-view />
    </main>
  </div>
</template>

<style scoped>
.app-nav {
  padding: 12px;
  background: #f0f0f0;
  display: flex;
  gap: 16px;
}
.router-link-active {
  color: #42b883;
  font-weight: bold;
}
</style>

现在运行应用,你可以在列表页、详情页和设置页之间无缝切换,浏览器前进后退按钮完美工作,URL 也始终同步。

此处插入图片:多页面Todo应用截图拼图,展示列表页、详情页和设置页


九、本篇总结

本文我们系统学习了 Vue Router,从前端路由的原理到环境配置,再到动态路由、嵌套路由、编程导航、路由守卫和元信息。通过重构 Todo 应用,你将组件化与路由结合起来,实现了真正意义上的单页应用。

关键收获

  • 前端路由让 SPA 的 URL 变化可控,主要有 Hash 和 History 两种模式。

  • createRouter + createWebHashHistory 初始化路由,<router-view> 是出口,<router-link> 声明导航。

  • 动态路由用 :param 定义,useRoute().params 获取参数,复用组件时需 watch 参数变化。

  • 嵌套路由通过 children 实现布局复用,命名视图可渲染多个同级组件。

  • router.push/replace/go 进行编程式导航,paramsquery 各有适用场景。

  • 路由守卫体系(全局、独享、组件内)让你能够拦截导航,实现权限控制、数据预载等逻辑。

  • 利用 meta 和 TypeScript 扩展增强代码安全性和可维护性。

下篇预告 : 有了路由,我们还缺一个能全局共享状态、跨组件通信的利器------状态管理。下一篇将走进 Pinia,Vue 3 的官方状态管理库。你将会学到如何定义 Store、使用 State/Getters/Actions,在 Todo 应用中实现数据持久化与多页面共享,以及与 Vue Devtools 的结合,让复杂应用的状态管理变得清晰而优雅。


总结 路由是 SPA 的骨架,它让应用从单一的页面变成了有机的整体。本文从原理到实战,覆盖了 Vue Router 在实际开发中最常用的知识和技巧。请务必跟着教程亲手重构 Todo 应用,当你点击"详情"看到 URL 变化、内容切换,却没有任何刷新时,就能真正理解前端路由的魅力。掌握路由,你就拥有了构建大型前端应用的导航能力。

此处插入图片:Vue Router 知识体系思维导图,涵盖路由配置、导航守卫、组合式 API 等

相关推荐
BomanGe21 小时前
NSK直线导轨LH55EL与NH55EM替代指南
前端·javascript·数据库·经验分享·规格说明书
糯米导航1 小时前
浏览器解析HTML头部的底层逻辑:从字节流到渲染树的关键一步
前端·html
风骏时光牛马1 小时前
C++开发常见问题与解决方案汇总
前端
用户83134859306981 小时前
Vue3+Cesium实现3DTiles模型实时调节(离地面高度/xyz轴旋转/模型经纬度偏移)
vue.js·cesium
zhedream1 小时前
Vue 3 Teleport 报错实录:从 patch 时机到 `defer` 属性
前端·vue.js
雁北向1 小时前
自定义指令 数值输入显示优化 巴飞特 测试
前端·vue.js
研☆香1 小时前
jQuery补充知识点
前端·javascript·jquery
先吃饱再说1 小时前
JavaScript栈和队列:从“冰柜里的雪糕”到“排队打饭”
javascript·数据结构
lichenyang4531 小时前
打车票根卡片 UI 重构:从 Circle 挖洞到 clipShape PathShape,再到 100% 自适应
前端