返回博客
技术 2025年3月7日 7 分钟阅读 · 1697 字
Vue Router 与 Pinia 状态管理实战
掌握 Vue 3 生态核心路由与状态管理方案,构建大型 SPA 应用
#Vue
#Vue Router
#Pinia
#状态管理
本文由 AI 辅助生成,经人工审核发布
Vue Router 4 路由配置
Vue Router 4 是为 Vue 3 量身打造的路由库,采用全新的 API 设计,支持 TypeScript,性能也更优。
基础配置
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
meta: { title: '首页', requiresAuth: false }
},
{
path: '/dashboard',
name: 'dashboard',
component: () => import('@/views/DashboardView.vue'),
meta: { title: '控制台', requiresAuth: true }
}
]
})
export default router
动态路由与嵌套路由
动态路由通过 :param 语法匹配 URL 参数,嵌套路由通过 children 实现页面布局复用。
const routes = [
{
path: '/user/:id',
component: UserLayout,
children: [
{
// /user/:id/profile
path: 'profile',
component: () => import('@/views/user/ProfileView.vue')
},
{
// /user/:id/settings
path: 'settings',
component: () => import('@/views/user/SettingsView.vue')
}
]
}
]
在组件中获取路由参数:
<script setup>
import { useRoute, watch } from 'vue'
const route = useRoute()
// 监听路由参数变化
watch(
() => route.params.id,
(newId) => {
console.log('用户ID变化:', newId)
}
)
</script>
<template>
<p>当前用户ID: {{ $route.params.id }}</p>
</template>
路由模式对比
| 模式 | 创建方式 | URL 形式 | 服务器配置 |
|---|---|---|---|
| History | createWebHistory() | /about | 需 fallback 配置 |
| Hash | createWebHashHistory() | /#/about | 无需额外配置 |
| Memory | createMemoryHistory() | 无 URL 变化 | SSR 场景使用 |
导航守卫
导航守卫是路由系统的核心功能,用于权限控制、数据预加载等场景。
全局守卫
router.beforeEach((to, from) => {
// 设置页面标题
document.title = to.meta.title || '默认标题'
// 鉴权
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } }
}
// 返回 true 或省略 return 表示放行
})
router.afterEach((to, from) => {
// 发送页面访问统计
trackPageView(to.path)
})
路由独享守卫与组件内守卫
// 路由独享守卫
{
path: '/admin',
component: AdminView,
beforeEnter: (to, from) => {
if (!isAdmin()) return { name: 'forbidden' }
}
}
// 组件内守卫
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 离开页面前的确认
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
return confirm('有未保存的更改,确定离开吗?')
}
})
// 路由更新时重新获取数据
onBeforeRouteUpdate((to) => {
if (to.params.id !== currentId.value) {
fetchData(to.params.id)
}
})
完整的导航解析流程
- 导航被触发
- 调用离开组件的
beforeRouteLeave - 调用全局
beforeEach - 在重用的组件里调用
beforeRouteUpdate - 调用路由配置中的
beforeEnter - 解析异步路由组件
- 调用进入组件的
beforeRouteEnter - 调用全局
beforeResolve - 导航被确认
- 调用全局
afterEach - 触发 DOM 更新
Pinia:Vue 3 官方状态管理
Pinia 是 Vue 官方推荐的状态管理库,相比 Vuex 更简洁、类型推导更好,且完全支持 Composition API。
定义 Store
Pinia 支持两种语法风格:Options Store 和 Setup Store。
// stores/counter.js
import { defineStore } from 'pinia'
// 方式一:Options Store
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo'
}),
getters: {
double: (state) => state.count * 2,
doublePlusOne() {
return this.double + 1
}
},
actions: {
increment() {
this.count++
},
async fetchCount() {
const res = await api.getCount()
this.count = res.data
}
}
})
// 方式二:Setup Store(推荐,与 Composition API 一致)
export const useUserStore = defineStore('user', () => {
const users = ref([])
const currentUser = ref(null)
const isAdmin = computed(() =>
currentUser.value?.role === 'admin'
)
const activeUsers = computed(() =>
users.value.filter(u => u.isActive)
)
async function fetchUsers() {
const res = await api.getUsers()
users.value = res.data
}
function setUser(user) {
currentUser.value = user
}
return {
users,
currentUser,
isAdmin,
activeUsers,
fetchUsers,
setUser
}
})
在组件中使用 Store
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const counter = useCounterStore()
// 解构 state 和 getter 需要用 storeToRefs 保持响应性
const { count, double } = storeToRefs(counter)
// action 可以直接解构
const { increment } = counter
// 直接修改 state
counter.$patch({ count: counter.count + 5 })
// 重置到初始状态
counter.$reset()
</script>
<template>
<button @click="increment">{{ count }} (x2: {{ double }})</button>
</template>
Store 间互相调用
Pinia 的 Store 可以直接互相引用,无需 Vuex 那样的模块注册:
// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const checkout = async () => {
const userStore = useUserStore()
if (!userStore.currentUser) {
throw new Error('请先登录')
}
return api.checkout({
userId: userStore.currentUser.id,
items: items.value
})
}
return { items, checkout }
})
Pinia 与 Vuex 对比
| 特性 | Vuex 4 | Pinia |
|---|---|---|
| API 风格 | Options API | Options + Setup |
| Mutation | 必须通过 mutation 修改 | 直接修改 state |
| TypeScript | 支持较弱,需复杂声明 | 原生支持,推导完善 |
| 模块系统 | modules 嵌套命名空间 | 扁平结构,各自独立 |
| Store 互调 | 通过 root 访问 | 直接导入使用 |
| 体积 | 较大 | 约 1KB |
| DevTools | 支持 | 支持,体验更好 |
状态持久化
刷新页面后 Pinia 状态会丢失,需要持久化方案。pinia-plugin-persistedstate 是最流行的插件:
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// 在 Store 中配置持久化
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null
}),
persist: {
// 只持久化 token,不持久化 userInfo
pick: ['token'],
// 使用 sessionStorage 替代默认的 localStorage
storage: sessionStorage
}
})
对于复杂场景,可以手写持久化逻辑:
// 自定义持久化插件
function persistPlugin({ store, options }) {
if (!options.persist) return
const key = `pinia:${store.$id}`
// 恢复
const saved = localStorage.getItem(key)
if (saved) {
store.$patch(JSON.parse(saved))
}
// 订阅变化,自动保存
store.$subscribe((mutation, state) => {
localStorage.setItem(key, JSON.stringify(state))
})
}
模块化拆分策略
大型项目中,将 Store 按业务域拆分是保持可维护性的关键:
src/stores/
├── index.js # pinia 实例与插件
├── auth.js # 认证相关
├── user.js # 用户管理
├── cart.js # 购物车
├── product.js # 商品
└── modules/
├── notification.js
└── settings.js
按功能聚合的组合式 Store
// stores/product.js
import { defineStore } from 'pinia'
export const useProductStore = defineStore('product', () => {
// state
const products = ref([])
const loading = ref(false)
const filters = reactive({
category: '',
priceRange: [0, 9999]
})
// getters
const filteredProducts = computed(() =>
products.value.filter(p =>
(!filters.category || p.category === filters.category) &&
p.price >= filters.priceRange[0] &&
p.price <= filters.priceRange[1]
)
)
const productCount = computed(() => products.value.length)
// actions
async function fetchProducts() {
loading.value = true
try {
const res = await api.getProducts()
products.value = res.data
} finally {
loading.value = false
}
}
function setFilter(key, value) {
filters[key] = value
}
return {
products,
loading,
filters,
filteredProducts,
productCount,
fetchProducts,
setFilter
}
})
跨 Store 数据流模式
当多个 Store 需要协同工作时,推荐在 action 中直接调用另一个 Store,而非在组件中手动协调:
// 购物车结算时需要同时更新购物车和订单
export const useCartStore = defineStore('cart', () => {
async function checkout() {
const orderStore = useOrderStore()
const userStore = useUserStore()
const order = await api.createOrder({
userId: userStore.currentUser.id,
items: cartItems.value
})
// 更新订单 Store
orderStore.addOrder(order)
// 清空购物车
cartItems.value = []
return order
}
})
路由与状态管理的配合
路由守卫中结合 Pinia 做权限控制是大型应用的常见模式:
// router/index.js
router.beforeEach(async (to) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth) {
// 首次加载,尝试恢复登录状态
if (!authStore.isLoggedIn && authStore.token) {
try {
await authStore.fetchUserInfo()
} catch {
authStore.logout()
return { name: 'login', query: { redirect: to.fullPath } }
}
}
if (!authStore.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } }
}
// 角色权限校验
if (to.meta.roles && !to.meta.roles.includes(authStore.userInfo.role)) {
return { name: 'forbidden' }
}
}
})
总结
Vue Router 4 和 Pinia 构成了 Vue 3 生态的路由与状态管理双引擎:
- Vue Router 4:动态路由、嵌套路由、完整的导航守卫体系,覆盖复杂应用的路由需求
- Pinia:简洁的 API 设计,完整的 TypeScript 支持,Setup Store 与 Composition API 无缝衔接
- 持久化:通过插件或自定义方案实现状态持久化,避免页面刷新数据丢失
- 模块化:按业务域拆分 Store,通过跨 Store 调用处理复杂数据流
- 路由+状态协同:在导航守卫中结合 Store 做权限控制,实现完整的认证授权流程
这套方案能支撑从中小型到大型 SPA 应用的路由与状态管理需求,代码结构清晰,维护成本低。