在前 3 课中,我们已经掌握了 Vue 3 的核心语法、组件化开发,并完成了单页面的待办事项项目。但实际开发中,绝大多数应用都是 "多页面结构"(比如首页、列表页、详情页),且需要组件间共享数据(比如用户登录状态、购物车数据)。本节课将聚焦Vue Router(路由) 和Pinia(状态管理) 两大核心工具,解决 "多页面跳转" 和 "跨组件数据共享" 问题,让项目从 "单页面 Demo" 升级为 "贴近企业开发的多页面应用"。
一、课前准备:安装路由与状态管理依赖(5 分钟搞定)
本节课需要新增两个核心依赖:Vue Router(实现页面跳转)和Pinia(Vue 3 官方推荐状态管理工具,替代旧版 Vuex),安装步骤简单,直接执行指令即可。
1. 安装依赖(在现有项目中执行)
打开 VS Code 终端,确保当前处于第 3 课创建的my-first-vue-project项目目录下,输入以下两条指令,依次安装:
bash
运行
# 安装Vue Router(Vue 3适配版)
npm install vue-router@4
# 安装Pinia(状态管理工具)
npm install pinia
安装完成后,查看项目目录下的package.json文件,若出现vue-router和pinia的版本信息,说明安装成功。
2. 课前知识铺垫(不用深究,先建立认知)
- Vue Router :相当于应用的 "导航系统",负责管理页面之间的跳转规则,比如点击 "待办列表" 跳转到
/todo页面,点击 "我的" 跳转到/profile页面,核心是 "URL 路径与组件的映射关系"。 - Pinia:相当于应用的 "全局数据仓库",负责存储所有组件都需要共享的数据(比如待办列表、用户信息),解决 "组件间传值繁琐" 的问题 ------ 比如第 3 课中,只有父子组件能直接传值,而 Pinia 让任意组件都能访问和修改全局数据。
二、核心实操一:Vue Router 入门 ------ 实现多页面跳转
1. 步骤 1:创建路由配置文件(统一管理路由规则)
在src文件夹下新建router文件夹,再在其中新建index.js文件(路由核心配置文件),复制以下代码:
javascript
运行
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 导入需要跳转的组件(后续创建)
import Home from '../views/Home.vue'
import TodoList from '../views/TodoList.vue'
import TodoDetail from '../views/TodoDetail.vue'
// 路由规则:配置"URL路径"与"组件"的映射关系
const routes = [
{
path: '/', // 首页路径
name: 'Home',
component: Home // 对应首页组件
},
{
path: '/todo', // 待办列表页路径
name: 'TodoList',
component: TodoList // 对应待办列表组件
},
{
path: '/todo/:id', // 待办详情页(动态路由:id为参数,比如/todo/0对应第一个待办)
name: 'TodoDetail',
component: TodoDetail // 对应待办详情组件
}
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), // 路由模式(HTML5历史模式,URL无#)
routes // 引入上面定义的路由规则
})
export default router // 导出路由实例,供main.js使用
2. 步骤 2:创建路由对应的页面组件(views 文件夹)
在src文件夹下新建views文件夹(专门存放页面级组件,与components的可复用小组件区分),创建 3 个组件文件:
(1)首页组件:Home.vue
vue
<template>
<div class="home">
<h1>Vue 多页面应用首页</h1>
<p>基于Vue Router + Pinia 实战</p>
<!-- 路由跳转链接:router-link替代a标签,避免页面刷新 -->
<router-link to="/todo" class="btn">进入待办列表</router-link>
</div>
</template>
<style scoped>
.home {
padding: 20px;
text-align: center;
}
.btn {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #42b983;
color: white;
text-decoration: none;
border-radius: 4px;
}
.btn:hover {
background-color: #359469;
}
</style>
(2)待办列表页:TodoList.vue(复用第 3 课的待办核心逻辑)
vue
<template>
<div class="todo-list-page">
<h2>待办事项列表</h2>
<!-- 新增待办区域 -->
<div class="add-todo">
<input
type="text"
v-model="newTodo"
placeholder="请输入新的待办事项"
>
<button @click="addTodo" class="add-btn">添加</button>
<button @click="clearAll" class="clear-btn">清空所有</button>
</div>
<!-- 待办列表 -->
<ul class="todo-list">
<li
v-for="(todo, index) in todoStore.todoList"
:key="index"
class="todo-item"
>
<!-- 跳转到详情页,传递index参数 -->
<router-link :to="`/todo/${index}`" class="todo-text">
{{ todo }}
</router-link>
<button @click="deleteTodo(index)" class="delete-btn">×</button>
</li>
</ul>
<!-- 跳转回首页 -->
<router-link to="/" class="back-btn">返回首页</router-link>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useTodoStore } from '../stores/todo' // 后续创建Pinia Store
const newTodo = ref('')
const todoStore = useTodoStore() // 获取待办数据的全局Store
// 新增待办(调用Store中的方法)
const addTodo = () => {
if (newTodo.value.trim() === '') return
todoStore.addTodo(newTodo.value.trim())
newTodo.value = ''
}
// 删除待办(调用Store中的方法)
const deleteTodo = (index) => {
todoStore.deleteTodo(index)
}
// 清空所有待办(调用Store中的方法)
const clearAll = () => {
todoStore.clearAll()
}
</script>
<style scoped>
.todo-list-page {
padding: 20px;
}
.add-todo {
margin: 20px 0;
display: flex;
gap: 10px;
}
.add-todo input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.add-btn {
padding: 8px 16px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.clear-btn {
padding: 8px 16px;
background-color: #f56c6c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
margin: 8px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
}
.todo-text {
color: #333;
text-decoration: none;
flex: 1;
}
.delete-btn {
background-color: transparent;
color: #f56c6c;
border: none;
font-size: 18px;
cursor: pointer;
padding: 0 8px;
}
.back-btn {
display: inline-block;
margin-top: 20px;
color: #42b983;
text-decoration: none;
}
</style>
(3)待办详情页:TodoDetail.vue(接收路由参数,展示单个待办)
vue
<template>
<div class="todo-detail">
<h2>待办详情</h2>
<div v-if="todo" class="todo-content">
<p>{{ todo }}</p>
<button @click="goBack" class="back-btn">返回列表</button>
</div>
<div v-else class="empty">
<p>该待办事项不存在!</p>
<router-link to="/todo" class="back-btn">返回待办列表</router-link>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTodoStore } from '../stores/todo'
const route = useRoute() // 获取当前路由信息(含参数)
const router = useRouter() // 编程式导航(用于返回上一页)
const todoStore = useTodoStore()
const todo = ref('')
// 页面加载时,根据路由参数获取待办详情
onMounted(() => {
const todoIndex = route.params.id // 获取路由传递的index参数
todo.value = todoStore.todoList[todoIndex] // 从Store中获取对应待办
})
// 编程式导航:返回上一页
const goBack = () => {
router.back()
}
</script>
<style scoped>
.todo-detail {
padding: 20px;
}
.todo-content {
padding: 20px;
border: 1px solid #eee;
border-radius: 4px;
margin: 20px 0;
}
.empty {
padding: 20px;
color: #f56c6c;
}
.back-btn {
padding: 8px 16px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
</style>
3. 步骤 3:在 main.js 中注册路由和 Pinia
修改src/main.js文件,引入并使用路由和 Pinia,让整个应用生效:
javascript
运行
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 引入Pinia
import router from './router' // 引入路由配置
import App from './App.vue'
import './style.css'
const app = createApp(App)
app.use(createPinia()) // 注册Pinia
app.use(router) // 注册路由
app.mount('#app')
4. 步骤 4:修改 App.vue,添加路由出口
将App.vue改为 "布局组件",使用router-view承载不同页面的内容(路由跳转时,这里会自动替换为对应页面组件):
vue
<template>
<div id="app">
<!-- 路由出口:所有页面都会渲染到这里 -->
<router-view />
</div>
</template>
<style scoped>
#app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
</style>
5. 路由核心功能实操验证
(1)基础路由跳转
启动项目(npm run dev),访问http://127.0.0.1:5173/,点击 "进入待办列表",URL 会变为/todo,页面切换为待办列表页;点击 "返回首页",URL 变回/,验证基础跳转功能。
(2)动态路由与参数传递
在待办列表页点击任意待办事项,URL 会变为/todo/0(0 为待办索引),页面切换到详情页并展示对应待办内容;若手动修改 URL 为/todo/100(不存在的索引),会显示 "该待办事项不存在",验证动态路由和参数接收功能。
(3)编程式导航
在待办详情页点击 "返回列表",会触发router.back(),返回上一页,验证编程式导航功能。
二、核心实操二:Pinia 状态管理入门 ------ 跨组件数据共享
1. 什么是 Pinia?为什么要用它?
第 3 课中,待办数据存储在App.vue组件中,只能在父子组件间传递;而实际开发中,多个非关联组件(比如首页和待办列表页)可能需要访问同一数据(比如用户信息、待办列表)。Pinia 相当于一个 "全局数据仓库",所有组件都能直接读取和修改其中的数据,无需手动传值。
2. 步骤 1:创建 Pinia Store(全局数据仓库)
在src文件夹下新建stores文件夹(专门存放 Pinia Store),创建todo.js文件(待办数据的 Store):
javascript
运行
// src/stores/todo.js
import { defineStore } from 'pinia'
// 定义并导出Store,命名规则:useXxxStore
export const useTodoStore = defineStore('todo', {
// 存储的全局状态(相当于组件的data)
state: () => ({
// 待办列表数据(从第3课迁移过来,初始值不变)
todoList: ['学习Vue核心语法', '完成第一个项目', '学习组件化']
}),
// 操作状态的方法(相当于组件的methods,推荐在这里修改状态,便于维护)
actions: {
// 新增待办
addTodo(todoText) {
this.todoList.push(todoText)
},
// 删除待办
deleteTodo(index) {
this.todoList.splice(index, 1)
},
// 清空所有待办
clearAll() {
this.todoList = []
}
},
// 计算属性(相当于组件的computed,可选)
getters: {
// 统计待办事项数量
todoCount() {
return this.todoList.length
}
}
})
3. 步骤 2:在组件中使用 Pinia Store
(1)读取 Store 中的数据
比如在Home.vue中展示待办数量(调用 getters):
vue
<template>
<div class="home">
<h1>Vue 多页面应用首页</h1>
<p>基于Vue Router + Pinia 实战</p>
<p>当前待办数量:{{ todoStore.todoCount }}</p> <!-- 读取getters -->
<router-link to="/todo" class="btn">进入待办列表</router-link>
</div>
</template>
<script setup>
import { useTodoStore } from '../stores/todo'
const todoStore = useTodoStore() // 获取Store实例
</script>
(2)修改 Store 中的数据
所有组件都能通过调用 Store 的actions方法修改数据,比如在TodoList.vue中新增、删除待办(已在前面的组件代码中实现),修改后的数据会全局同步 ------ 比如在首页看到的待办数量会实时更新。
4. Pinia 核心特性验证
(1)数据共享
在待办列表页新增一个待办事项,返回首页,会发现 "当前待办数量" 自动增加,验证跨组件数据同步。
(2)状态修改规范
推荐通过actions方法修改状态(而非直接修改state),比如禁止todoStore.todoList.push('xxx'),而是调用todoStore.addTodo('xxx'),这样便于后续维护和调试(可在actions中添加日志、校验等逻辑)。
三、综合实战:升级待办事项为多页面 + 全局状态应用
1. 实战目标
实现一个完整的多页面待办应用,包含 3 大核心功能:
- 首页:展示待办数量,提供跳转到待办列表的入口;
- 待办列表页:新增、删除、清空待办,点击待办跳转到详情页;
- 待办详情页:展示单个待办内容,支持返回列表页。
2. 完整流程测试
- 启动项目,访问首页,查看待办数量(初始 3 个);
- 点击 "进入待办列表",新增一个待办(比如 "学习 Vue Router"),返回首页,待办数量变为 4;
- 在待办列表页点击新增的待办,进入详情页,查看内容;
- 点击详情页 "返回列表",回到待办列表,删除该待办,首页数量变回 3;
- 点击 "清空所有",待办列表为空,首页数量变为 0。
3. 新手优化建议
- 给路由添加导航守卫:比如进入待办详情页前,判断索引是否有效,无效则自动跳回列表页;
- 在 Pinia 中添加本地存储:使用
localStorage保存待办数据,刷新页面后数据不丢失(提示:在actions中修改数据时同步到本地存储,state初始化时从本地存储读取); - 给待办列表添加编辑功能:在详情页新增 "编辑" 输入框,修改后同步到 Pinia。
四、本节课总结与下节课预告
1. 本节课核心收获
- 路由(Vue Router):掌握路由配置、页面跳转(
router-link/ 编程式导航)、动态路由与参数传递,实现多页面结构; - 状态管理(Pinia):掌握 Store 的创建、数据读取、通过
actions修改数据,解决跨组件数据共享问题; - 项目升级:将单页面 Demo 升级为多页面应用,贴近企业实际开发的技术栈组合。
2. 课后作业(必做)
- 独立复现本节课的路由配置和 Pinia Store,不看教程完成多页面待办应用;
- 实现优化需求:给待办数据添加本地存储(
localStorage),确保刷新页面后数据不丢失; - 新增 "编辑待办" 功能:在详情页添加编辑输入框,修改待办内容后同步到 Pinia 和本地存储;
- 整理路由和 Pinia 的踩坑笔记,比如 "路由参数类型是字符串,需要转数字""Pinia 的
state必须是函数" 等。
3. 下节课预告
下节课我们将学习 "Vue 3 HTTP 请求与 UI 库实战",解决 "调用后端接口获取数据" 和 "快速搭建美观界面" 的问题 ------ 比如调用免费的待办 API 实现数据持久化,使用 Element Plus 组件库优化页面样式,让项目从 "本地数据应用" 升级为 "前后端交互应用",进一步贴近企业开发场景。