第 3 章 · Vue Router 与 Pinia 实战
本章目标:为 academy-demo-vue 接入 vue-router 多页面路由与 Pinia 全局状态;实现首页 / 课程列表 / 课程详情 / 待办四页;与 Fetch Mock 联调,对齐 ch16 毕业项目核心链路。
学时建议:4~5 小时
前置:完成本模块 ch01~ch02;熟悉 ch16 数据模型与 Mock 结构。
3.1 安装与项目结构
cd academy-demo-vue
npm install vue-router@4 pinia
src/
├── main.js
├── App.vue # <RouterView />
├── router/
│ └── index.js
├── stores/
│ ├── courses.js # 课程列表、筛选
│ ├── favorites.js # 收藏
│ └── userPrefs.js # 主题、最近搜索
├── views/
│ ├── HomeView.vue
│ ├── CourseListView.vue
│ ├── CourseDetailView.vue
│ └── TodoView.vue
├── components/
│ ├── AppHeader.vue
│ ├── CourseCard.vue
│ └── CourseList.vue
└── api/
└── client.js
| 模块 | 职责 |
|---|---|
router/ | URL ↔ 视图映射、导航守卫 |
stores/ | 跨页面共享状态 |
views/ | 页面级组件(对应 ch16 各 html) |
api/client.js | 统一 Fetch 封装 |
3.2 Vue Router 基础配置
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue'),
meta: { title: '首页' },
},
{
path: '/courses',
name: 'courses',
component: () => import('../views/CourseListView.vue'),
meta: { title: '课程列表' },
},
{
path: '/courses/:id',
name: 'course-detail',
component: () => import('../views/CourseDetailView.vue'),
props: true,
meta: { title: '课程详情' },
},
{
path: '/todo',
name: 'todo',
component: () => import('../views/TodoView.vue'),
meta: { title: '学习计划' },
},
{ path: '/:pathMatch(.*)*', redirect: '/' },
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior() {
return { top: 0 }
},
})
router.afterEach((to) => {
document.title = `${to.meta.title ?? '页面'} · 贤紫技术学院`
})
export default router
// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
<!-- src/App.vue -->
<template>
<AppHeader />
<main class="main">
<RouterView />
</main>
<footer class="site-footer">© 2026 贤紫优选</footer>
</template>
3.3 动态路由与导航
详情页通过 props: true 接收 :id;useRoute() 读参数,useRouter() 编程式跳转。AppHeader 用 RouterLink + router-link-active 高亮当前页。列表卡片:router.push({ name: 'course-detail', params: { id } })。
无效 id 时 router.replace({ name: 'courses' });onMounted 先 fetchAll() 再 fetchChapters(id)。
3.4 导航守卫
| 守卫 | 触发时机 | 典型用途 |
|---|---|---|
beforeEach | 全局跳转前 | 鉴权、进度条 |
beforeEnter | 进入某路由前 | 预加载数据 |
onBeforeRouteLeave | 组件内离开 | 未保存提示 |
// router/index.js 片段
router.beforeEach((to, from, next) => {
// 示例:详情页必须有 id
if (to.name === 'course-detail' && !to.params.id) {
next({ name: 'courses' })
return
}
next()
})
<!-- TodoView.vue 离开守卫示例 -->
onBeforeRouteLeave((to, from, next) => {
dirty.value && !confirm('待办未保存?') ? next(false) : next()
})
3.5 Pinia Store 设计
3.5.1 课程 Store
// src/stores/courses.js