Vue3组合式API状态管理实战:Pinia与Composable函数的架构选型

Vue3状态管理为什么需要重新选型

Vue3组合式API的引入让状态管理发生了根本性变化。Options API时代,Vuex是唯一选项,但Composable函数天然具备状态封装能力,很多场景不再需要全局Store。前端开发中状态管理的核心问题是:什么时候用Composable函数就够了,什么时候必须引入Pinia?

判断标准很直接——状态是否需要跨组件树共享。如果只在组件及其子组件间传递,Composable + provide/inject即可;如果多个不相关组件需要读写同一份状态,Pinia是更合理的方案。

Composable函数封装局部状态

Composable函数是Vue3最优雅的状态封装方式,用refcomputed即可构建自包含的状态逻辑:

// composables/useFilter.ts
import { ref, computed } from 'vue'

export function useFilter<T>(items: Ref<T[]>, keyFn: (item: T) => string) {
  const keyword = ref('')
  const filtered = computed(() => {
    if (!keyword.value) return items.value
    return items.value.filter(item =>
      keyFn(item).toLowerCase().includes(keyword.value.toLowerCase())
    )
  })

  function setFilter(val: string) {
    keyword.value = val
  }

  return { keyword, filtered, setFilter }
}

使用方式:

<script setup lang="ts">
import { useFilter } from '@/composables/useFilter'

const products = ref<Product[]>([...])
const { filtered, setFilter } = useFilter(products, item => item.name)
</script>

<template>
  <input :value="keyword" @input="setFilter($event.target.value)" />
  <ul>
    <li v-for="item in filtered" :key="item.id">{{ item.name }}</li>
  </ul>
</template>

Composable函数的状态生命周期与使用它的组件绑定,组件销毁时状态自动清理。这种模式适合搜索筛选、表单校验、分页等局部状态场景。

Pinia全局Store的架构设计

Pinia相比Vuex的优势:去掉了Mutations的冗余层,支持Composition API风格定义Store,天然支持TypeScript推导,支持多Store组合。

定义Store的标准模式:

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // State
  const token = ref('')
  const userInfo = ref<UserInfo | null>(null)
  const permissions = ref<string[]>([])

  // Getters
  const isLoggedIn = computed(() => !!token.value)
  const hasPermission = computed(() => (perm: string) => permissions.value.includes(perm))

  // Actions
  async function login(credentials: LoginParams) {
    const res = await authApi.login(credentials)
    token.value = res.token
    userInfo.value = res.user
    permissions.value = res.permissions
    localStorage.setItem('token', res.token)
  }

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

  function loadFromStorage() {
    const saved = localStorage.getItem('token')
    if (saved) token.value = saved
  }

  return { token, userInfo, permissions, isLoggedIn, hasPermission, login, logout, loadFromStorage }
})

Store组合——一个Store引用另一个Store的数据:

// stores/cart.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', () => {
  const userStore = useUserStore()
  const items = ref<CartItem[]>([])

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  async function addItem(product: Product) {
    if (!userStore.isLoggedIn) {
      throw new Error('请先登录')
    }
    // 业务逻辑...
  }

  return { items, total, addItem }
})

Pinia插件扩展:持久化与重置策略

Pinia插件机制支持全局状态持久化、重置策略、DevTools集成等横切关注点:

// plugins/persisted.ts
import { PiniaPluginContext } from 'pinia'

export function piniaPersistPlugin({ store }: PiniaPluginContext) {
  const storageKey = `pinia-${store.$id}`

  // 从localStorage恢复状态
  const saved = localStorage.getItem(storageKey)
  if (saved) {
    store.$patch(JSON.parse(saved))
  }

  // 监听状态变化并持久化
  store.$subscribe((mutation, state) => {
    localStorage.setItem(storageKey, JSON.stringify(state))
  })
}

// 注册插件
const pinia = createPinia()
pinia.use(piniaPersistPlugin)

生产级持久化需要考虑更多细节——哪些字段需要持久化、哪些需要排除(token过期后不能复用)、序列化失败的处理:

// 生产级持久化插件
export function piniaPersistPlugin({ store }: PiniaPluginContext) {
  const options = store.$id
  const storageKey = `app-${store.$id}`

  // 可在Store定义中声明需要持久化的字段
  // const useUserStore = defineStore('user', () => { ... }, {
  //   persist: { paths: ['token', 'userInfo'], exclude: ['permissions'] }
  // })
  const persistPaths = (store.$id as any).persist?.paths

  const saved = localStorage.getItem(storageKey)
  if (saved) {
    try {
      const data = JSON.parse(saved)
      store.$patch(persistPaths
        ? persistPaths.reduce((acc, key) => { acc[key] = data[key]; return acc }, {} as any)
        : data
      )
    } catch (e) {
      localStorage.removeItem(storageKey)
    }
  }

  store.$subscribe(() => {
    try {
      const state = store.$state
      const data = persistPaths
        ? persistPaths.reduce((acc, key) => { acc[key] = state[key]; return acc }, {} as any)
        : state
      localStorage.setItem(storageKey, JSON.stringify(data))
    } catch (e) {
      console.error(`Failed to persist store ${store.$id}`, e)
    }
  })
}

Composable与Pinia的混合架构模式

实际项目中两种模式往往混合使用。架构分层原则:

– 视图层(组件内):Composable管理UI状态(表单、弹窗、筛选条件)
– 业务层(Store):Pinia管理领域状态(用户信息、购物车、权限)
– 基础设施层:Composable封装三方SDK(地图、支付、IM)

// composables/useMap.ts - 封装第三方地图SDK
export function useMap(container: Ref<HTMLElement | null>) {
  const mapInstance = ref<any>(null)
  const isReady = ref(false)

  onMounted(() => {
    mapInstance.value = new AMap.Map(container.value, {
      zoom: 12,
      center: [116.397, 39.908]
    })
    mapInstance.value.on('complete', () => {
      isReady.value = true
    })
  })

  onUnmounted(() => {
    mapInstance.value?.destroy()
  })

  function addMarker(lng: number, lat: number) {
    if (!isReady.value) return
    new AMap.Marker({ position: [lng, lat], map: mapInstance.value })
  }

  return { isReady, addMarker }
}

组件中的使用层次清晰:

<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { useFilter } from '@/composables/useFilter'
import { useMap } from '@/composables/useMap'

// 全局Store
const userStore = useUserStore()
const cartStore = useCartStore()

// 局部Composable
const mapRef = ref<HTMLElement | null>(null)
const { isReady, addMarker } = useMap(mapRef)
const { filtered, setFilter } = useFilter(
  computed(() => cartStore.items),
  item => item.name
)
</script>

响应式性能优化:shallowRef与triggerRef

Pinia Store中的大型对象(如列表数据、嵌套JSON),Vue3默认深度响应式代理会带来可观的开销。当对象层级深、数据量大时,用shallowRef + triggerRef手动控制响应式触发时机:

// stores/products.ts
import { shallowRef, triggerRef } from 'vue'

export const useProductStore = defineStore('products', () => {
  // 大列表用shallowRef避免深度代理
  const products = shallowRef<Product[]>([])

  async function fetchProducts() {
    const data = await productApi.list()
    products.value = data
    // shallowRef需要手动触发更新
    triggerRef(products)
  }

  function updateProduct(id: number, patch: Partial<Product>) {
    const idx = products.value.findIndex(p => p.id === id)
    if (idx !== -1) {
      // 创建新数组引用触发更新
      const newList = [...products.value]
      newList[idx] = { ...newList[idx], ...patch }
      products.value = newList
    }
  }

  return { products, fetchProducts, updateProduct }
})

性能对比:在10000条商品数据的场景下,shallowRefref的初始化速度快6倍以上,更新单条数据时差异更显著——ref需要遍历整个数组做深度代理,shallowRef只替换引用。

Pinia Store的单元测试策略

Pinia Store的测试比Vuex简单很多,直接创建Store实例测试即可:

// stores/__tests__/user.test.ts
import { createPinia, setActivePinia } from 'pinia'
import { useUserStore } from '../user'
import { describe, it, expect, beforeEach, vi } from 'vitest'

describe('useUserStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('初始状态未登录', () => {
    const store = useUserStore()
    expect(store.isLoggedIn).toBe(false)
    expect(store.token).toBe('')
  })

  it('登录后状态正确', async () => {
    const store = useUserStore()
    // Mock API
    vi.spyOn(authApi, 'login').mockResolvedValue({
      token: 'test-token',
      user: { id: 1, name: 'test' },
      permissions: ['read']
    })

    await store.login({ username: 'test', password: '123' })

    expect(store.token).toBe('test-token')
    expect(store.isLoggedIn).toBe(true)
    expect(store.hasPermission('read')).toBe(true)
  })

  it('登出后清除状态', () => {
    const store = useUserStore()
    store.token = 'test-token'
    store.logout()
    expect(store.token).toBe('')
    expect(store.isLoggedIn).toBe(false)
  })
})

Composable函数的测试用@vue/test-utilsmount或直接在setup上下文中测试:

// composables/__tests__/useFilter.test.ts
import { ref } from 'vue'
import { useFilter } from '../useFilter'
import { describe, it, expect } from 'vitest'

describe('useFilter', () => {
  it('按关键字过滤列表', () => {
    const items = ref([
      { id: 1, name: 'Apple' },
      { id: 2, name: 'Banana' },
      { id: 3, name: 'Cherry' }
    ])
    const { filtered, setFilter } = useFilter(items, item => item.name)

    setFilter('ap')
    expect(filtered.value).toHaveLength(1)
    expect(filtered.value[0].name).toBe('Apple')
  })
})

以上方案从Composable局部状态、Pinia全局Store、插件扩展、混合架构到性能优化和测试,覆盖了Vue3组合式API下状态管理的完整架构选型与实践路径。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zhuang-tai-guan-li-shi-zhan-pinia-yu-2/

(0)
小编小编
上一篇 19小时前
下一篇 19小时前

相关推荐

发表回复

登录后才能评论