Vue3 整合 Pinia 和 Vue Router

今天我们将把前几天的零散组件串联起来,构建一个真正的单页应用 (SPA)
我们需要两位得力助手:

  1. Vue Router 4:负责页面跳转、路由守卫、参数传递。
  2. Pinia:负责全局状态管理(取代 Vuex),让数据在任意组件间轻松共享。

📦 一、快速安装与初始化

假设你已经有一个 Vite + Vue 3 项目:

# 安装路由和状态管理
npm install vue-router@4 pinia

1. 初始化 Pinia (src/main.js)

Pinia 的使用非常简单,只需创建一个实例并挂载到 App 上。

// src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 引入 Pinia
import App from './App.vue'
import router from './router' // 引入稍后创建的路由

const app = createApp(App)
const pinia = createPinia()

app.use(pinia) // 使用 Pinia
app.use(router) // 使用 Router

app.mount('#app')

🗺️ 二、Vue Router 4:掌控导航

Router 是 SPA 的骨架。Vue 3 中推荐使用基于组合式 API 的写法。

1. 配置路由 (src/router/index.js)

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import UserView from '../views/UserView.vue'
import NotFound from '../views/NotFound.vue'

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/user/:id', // 动态路由参数
    name: 'user',
    component: UserView,
    props: true // 🔥 重要技巧:将 route.params 直接作为 props 传给组件
  },
  {
    path: '/:pathMatch(.*)*', // 404 通配符
    name: 'not-found',
    component: NotFound
  }
]

const router = createRouter({
  history: createWebHistory(), // 使用 HTML5 History 模式 (URL 无 #)
  routes
})

export default router

2. 设置入口 (src/App.vue)

<template>
  <nav>
    <!-- 声明式导航 -->
    <router-link to="/">首页</router-link> |
    <router-link to="/user/123">用户详情 (ID: 123)</router-link>
  </nav>

  <!-- 路由出口:匹配的组件将渲染在这里 -->
  <router-view />
</template>

<style>
/* 激活链接的样式 */
.router-link-active {
  font-weight: bold;
  color: #42b983;
}
</style>

3. 在组件中使用路由 (UserView.vue)

<script setup> 中,不再需要 this.$route,而是使用 Hooks。

<script setup>
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'

// 1. 获取当前路由信息 (相当于 this.$route)
const route = useRoute()

// 2. 获取路由实例用于编程式导航 (相当于 this.$router)
const router = useRouter()

// 如果开启了 props: true,可以直接接收 id
defineProps({
  id: {
    type: String,
    required: true
  }
})

// 或者从 route.params 获取 (如果没开 props)
// const userId = computed(() => route.params.id)

const goHome = () => {
  router.push('/') // 跳转
}

const goToUser = (id) => {
  router.push(`/user/${id}`)
}
</script>

<template>
  <div>
    <h2>用户详情页</h2>
    <p>当前用户 ID: {{ id }}</p>
    
    <button @click="goHome">返回首页</button>
    <button @click="goToUser('456')">跳转到用户 456</button>
    
    <!-- 显示当前所有参数 -->
    <pre>{{ route.query }}</pre> 
  </div>
</template>

🛡️ 进阶:路由守卫 (全局前置守卫)

常用于权限验证(如:未登录不能访问后台)。

// src/router/index.js (续)
router.beforeEach((to, from, next) => {
  const isAuthenticated = false // 模拟登录状态
  
  if (to.name !== 'home' && !isAuthenticated) {
    // 重定向到登录页,保留原目标地址
    next({ name: 'home', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

🐹 三、Pinia:轻量级状态管理

Pinia 去掉了 Vuex 中的 mutation,只保留 state, getters, actions,并且对 TypeScript 支持极佳。

1. 创建 Store (src/stores/counter.js)

// src/stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 第一个参数是 Store 的唯一 ID (必填)
export const useCounterStore = defineStore('counter', () => {
  // 1. State (响应式数据)
  const count = ref(0)
  const name = ref('Pinia Store')

  // 2. Getters (计算属性)
  const doubleCount = computed(() => count.value * 2)

  // 3. Actions (方法,支持异步)
  function increment() {
    count.value++
  }

  function incrementAsync() {
    setTimeout(() => {
      count.value++
    }, 1000)
  }
  
  // 也可以写成箭头函数
  const decrement = () => count.value--

  return { count, name, doubleCount, increment, incrementAsync, decrement }
})

💡 为什么用函数式定义?
上面的写法叫 "Setup Store",它和组件的 <script setup> 逻辑完全一致,支持任意组合式 API,是 Pinia 推荐的新写法。

2. 在组件中使用 Store

<script setup>
import { storeToRefs } from 'pinia' // 🔥 关键:用于解构保持响应性
import { useCounterStore } from '../stores/counter'

const counterStore = useCounterStore()

// ❌ 错误做法:直接解构会丢失响应性
// const { count } = counterStore 

// ✅ 正确做法 1:直接通过 store 访问
// <button @click="counterStore.increment">{{ counterStore.count }}</button>

// ✅ 正确做法 2:如果需要解构,使用 storeToRefs
// 这会将 count 和 doubleCount 转换为 ref,保持响应式
const { count, doubleCount, name } = storeToRefs(counterStore)
const { increment, decrement } = counterStore // 方法不需要 storeToRefs
</script>

<template>
  <div class="store-demo">
    <h3>Pinia 状态演示</h3>
    <p>商店名称: {{ name }}</p>
    <p>计数: {{ count }} (双倍: {{ doubleCount }})</p>
    
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
    <button @click="counterStore.incrementAsync" :disabled="count > 10">
      异步 +1 (1秒后)
    </button>
  </div>
</template>

3. 跨组件共享状态

Pinia 的核心优势:状态是单例的
如果你在 ComponentA 中调用了 increment()ComponentB 中的 count 会自动更新,无需任何额外代码(只要它们都引用了同一个 Store ID)。


🔄 四、整合实战:路由 + Store

场景:用户登录后,将用户信息存入 Pinia,并在路由守卫中检查。

1. 用户 Store (src/stores/user.js)

import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('user', () => {
  const token = ref(localStorage.getItem('token') || '')
  const userInfo = ref(null)

  const isLoggedIn = computed(() => !!token.value)

  function login(newToken, user) {
    token.value = newToken
    userInfo.value = user
    localStorage.setItem('token', newToken)
  }

  function logout() {
    token.value = ''
    userInfo.value = null
    localStorage.removeItem('token')
  }

  return { token, userInfo, isLoggedIn, login, logout }
})

2. 路由守卫集成

// src/router/index.js
import { useUserStore } from '../stores/user'

router.beforeEach((to, from, next) => {
  const userStore = useUserStore() // 在守卫中直接使用 store
  
  // 假设 /admin 需要登录
  if (to.path.startsWith('/admin') && !userStore.isLoggedIn) {
    next('/login?redirect=' + to.fullPath)
  } else {
    next()
  }
})

🚫 Day 4 避坑指南

  1. 解构 Store 丢失响应性

    • 现象const { count } = useCounterStore(),修改 count 视图不更新。
    • 解决:要么直接用 store.count,要么用 storeToRefs(store)
    • 例外:Actions (方法) 可以直接解构,因为它们不是响应式数据。
  2. Router 循环跳转

    • beforeEach 中调用 next() 时,务必确保不会形成死循环(例如:未登录 -> 跳登录页 -> 登录页守卫又判断未登录 -> 跳登录页...)。通常登录页本身不需要登录守卫。
  3. Devtools 插件

    • 记得安装 Vue Devtools 浏览器扩展。Pinia 和 Router 都有专门的面板,可以实时查看状态变化和路由历史,调试神器。
  4. SSR 注意事项

    • 如果你未来要做服务端渲染 (Nuxt.js),Pinia 和 Router 的初始化方式会有所不同(需要在每个请求中创建新实例),但在纯客户端 Vite 项目中,上述写法完全没问题。

🎯 今日实战练习

任务:构建一个简单的“待办事项 (Todo) 应用”原型。

  1. 路由
    • / (首页): 显示所有 Todo。
    • /completed: 仅显示已完成的 Todo。
  2. Store (useTodoStore)
    • State: todos (数组)。
    • Actions: addTodo(text), toggleTodo(id), clearCompleted()
    • Getters: completedTodos, pendingTodos
  3. 功能
    • 在首页输入框添加任务。
    • 点击任务切换完成状态。
    • 顶部导航栏切换 //completed 视图。
    • /completed 页面显示一个“清空已完成”按钮。

思考题:如果我想在刷新页面后保留 Todo 数据,应该在哪里加代码?(提示:watch + localStorage 或 Pinia 持久化插件)。


🏁 总结

至此,你已经掌握了 Vue 3 开发大型应用的完整拼图:

  • UI 层: <script setup>, Components, Props/Emits.
  • 逻辑层: Composables (useXxx).
  • 数据层: Pinia (全局状态).
  • 导航层: Vue Router (页面流转).
posted @ 2026-03-02 17:54  AI小羊仔  阅读(171)  评论(0)    收藏  举报