第4 课:Vue 3 路由与状态管理实战 —— 从单页面到多页面应用

在前 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-routerpinia的版本信息,说明安装成功。

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 大核心功能:

  1. 首页:展示待办数量,提供跳转到待办列表的入口;
  2. 待办列表页:新增、删除、清空待办,点击待办跳转到详情页;
  3. 待办详情页:展示单个待办内容,支持返回列表页。

2. 完整流程测试

  1. 启动项目,访问首页,查看待办数量(初始 3 个);
  2. 点击 "进入待办列表",新增一个待办(比如 "学习 Vue Router"),返回首页,待办数量变为 4;
  3. 在待办列表页点击新增的待办,进入详情页,查看内容;
  4. 点击详情页 "返回列表",回到待办列表,删除该待办,首页数量变回 3;
  5. 点击 "清空所有",待办列表为空,首页数量变为 0。

3. 新手优化建议

  1. 给路由添加导航守卫:比如进入待办详情页前,判断索引是否有效,无效则自动跳回列表页;
  2. 在 Pinia 中添加本地存储:使用localStorage保存待办数据,刷新页面后数据不丢失(提示:在actions中修改数据时同步到本地存储,state初始化时从本地存储读取);
  3. 给待办列表添加编辑功能:在详情页新增 "编辑" 输入框,修改后同步到 Pinia。

四、本节课总结与下节课预告

1. 本节课核心收获

  • 路由(Vue Router):掌握路由配置、页面跳转(router-link/ 编程式导航)、动态路由与参数传递,实现多页面结构;
  • 状态管理(Pinia):掌握 Store 的创建、数据读取、通过actions修改数据,解决跨组件数据共享问题;
  • 项目升级:将单页面 Demo 升级为多页面应用,贴近企业实际开发的技术栈组合。

2. 课后作业(必做)

  1. 独立复现本节课的路由配置和 Pinia Store,不看教程完成多页面待办应用;
  2. 实现优化需求:给待办数据添加本地存储(localStorage),确保刷新页面后数据不丢失;
  3. 新增 "编辑待办" 功能:在详情页添加编辑输入框,修改待办内容后同步到 Pinia 和本地存储;
  4. 整理路由和 Pinia 的踩坑笔记,比如 "路由参数类型是字符串,需要转数字""Pinia 的state必须是函数" 等。

3. 下节课预告

下节课我们将学习 "Vue 3 HTTP 请求与 UI 库实战",解决 "调用后端接口获取数据" 和 "快速搭建美观界面" 的问题 ------ 比如调用免费的待办 API 实现数据持久化,使用 Element Plus 组件库优化页面样式,让项目从 "本地数据应用" 升级为 "前后端交互应用",进一步贴近企业开发场景。

相关推荐
沐泽__2 小时前
iframe内嵌页面双向通信
前端·javascript·chrome
ohyeah2 小时前
用 Vue3 + Coze API 打造冰球运动员 AI 生成器:从图片上传到风格化输出
前端·vue.js·coze
interception2 小时前
爬虫逆向,瑞数6,补环境,国家专利
javascript·爬虫·python·网络爬虫
Dragon Wu2 小时前
TailWindCss 核心功能总结
前端·css·前端框架·postcss
SHolmes18543 小时前
给定某日的上班时间段,计算当日的工作时间总时长(Python)
开发语言·前端·python
掘金安东尼3 小时前
顶层元素问题:popover vs. dialog
前端·javascript·面试
掘金安东尼3 小时前
React 的新时代已经到来:你需要知道的一切
前端·javascript·面试
掘金安东尼3 小时前
React 已经改变了,你的 Hooks 也应该改变
前端·vue.js·github
Codebee3 小时前
A2UI vs OOD全栈方案:AI驱动UI的两种技术路径深度解析
前端·架构