返回博客
技术 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 形式服务器配置
HistorycreateWebHistory()/about需 fallback 配置
HashcreateWebHashHistory()/#/about无需额外配置
MemorycreateMemoryHistory()无 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)
  }
})

完整的导航解析流程

  1. 导航被触发
  2. 调用离开组件的 beforeRouteLeave
  3. 调用全局 beforeEach
  4. 在重用的组件里调用 beforeRouteUpdate
  5. 调用路由配置中的 beforeEnter
  6. 解析异步路由组件
  7. 调用进入组件的 beforeRouteEnter
  8. 调用全局 beforeResolve
  9. 导航被确认
  10. 调用全局 afterEach
  11. 触发 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 4Pinia
API 风格Options APIOptions + 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 生态的路由与状态管理双引擎:

  1. Vue Router 4:动态路由、嵌套路由、完整的导航守卫体系,覆盖复杂应用的路由需求
  2. Pinia:简洁的 API 设计,完整的 TypeScript 支持,Setup Store 与 Composition API 无缝衔接
  3. 持久化:通过插件或自定义方案实现状态持久化,避免页面刷新数据丢失
  4. 模块化:按业务域拆分 Store,通过跨 Store 调用处理复杂数据流
  5. 路由+状态协同:在导航守卫中结合 Store 做权限控制,实现完整的认证授权流程

这套方案能支撑从中小型到大型 SPA 应用的路由与状态管理需求,代码结构清晰,维护成本低。