Vue3组合式API状态管理:Pinia与Composables协作模式实战

Pinia与Composables的分工边界

Vue3组合式API带来了两种状态管理思路:Pinia管理全局共享状态,Composables封装组件级可复用逻辑。实际项目中两者的边界经常模糊——订单数据放Pinia还是Composable?分页逻辑写在哪?搞清这个问题是组织好Vue3状态管理的前提。

核心判断原则:跨组件共享且需要持久化的数据归Pinia,组件内或有限复用的逻辑归Composable。用户信息、购物车、权限列表属于Pinia;表单校验、分页、防抖请求属于Composable。

Pinia Store设计与模块拆分

大型应用的Store不应该把所有状态塞进一个文件。按业务领域拆分,每个Store职责单一。

用户认证Store

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

export const useUserStore = defineStore('user', () => {
  const token = ref<string>('')
  const userInfo = ref<UserInfo | null>(null)
  const isLoggedIn = computed(() => !!token.value)
  const permissions = computed(() => userInfo.value?.permissions ?? [])

  async function login(credentials: LoginForm) {
    const res = await api.login(credentials)
    token.value = res.token
    userInfo.value = res.user
    localStorage.setItem('token', res.token)
  }

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

  function hasPermission(perm: string): boolean {
    return permissions.value.includes(perm)
  }

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

购物车Store

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

export const useCartStore = defineStore('cart', () => {
  interface CartItem {
    id: string
    name: string
    price: number
    quantity: number
  }

  const items = ref<CartItem[]>([])

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

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

  function addItem(product: Omit<CartItem, 'quantity'>) {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  function removeItem(productId: string) {
    const index = items.value.findIndex(i => i.id === productId)
    if (index > -1) items.value.splice(index, 1)
  }

  return { items, totalCount, totalPrice, addItem, removeItem }
})

Composable封装组件级可复用逻辑

Composable的本质是把setup中的响应式逻辑提取为独立函数。与Pinia的区别:Composable每次调用创建独立的响应式实例,不跨组件共享。

分页逻辑Composable

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

export function usePagination(options: {
  total: () => number
  pageSize?: number
}) {
  const currentPage = ref(1)
  const pageSize = ref(options.pageSize ?? 20)

  const totalPages = computed(() =>
    Math.ceil(options.total() / pageSize.value)
  )

  const paginatedRange = computed(() => {
    const start = (currentPage.value - 1) * pageSize.value
    const end = start + pageSize.value
    return { start, end }
  })

  function goToPage(page: number) {
    if (page < 1 || page > totalPages.value) return
    currentPage.value = page
  }

  function nextPage() {
    goToPage(currentPage.value + 1)
  }

  function prevPage() {
    goToPage(currentPage.value - 1)
  }

  watch(() => options.total(), () => {
    if (currentPage.value > totalPages.value) {
      currentPage.value = Math.max(1, totalPages.value)
    }
  })

  return {
    currentPage, pageSize, totalPages,
    paginatedRange, goToPage, nextPage, prevPage,
  }
}

防抖请求Composable

// composables/useDebouncedFetch.ts
import { ref, onUnmounted } from 'vue'

export function useDebouncedFetch<T>(fn: (params: string) => Promise<T>, delay = 300) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)
  let timer: ReturnType<typeof setTimeout> | null = null

  async function execute(params: string) {
    if (timer) clearTimeout(timer)

    return new Promise<void>((resolve) => {
      timer = setTimeout(async () => {
        loading.value = true
        error.value = null
        try {
          data.value = await fn(params)
        } catch (e) {
          error.value = e as Error
        } finally {
          loading.value = false
          resolve()
        }
      }, delay)
    })
  }

  onUnmounted(() => {
    if (timer) clearTimeout(timer)
  })

  return { data, loading, error, execute }
}

Pinia与Composable在组件中的协作

在组件中同时使用Store和Composable,数据流清晰可控:

<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
import { usePagination } from '@/composables/usePagination'
import { useDebouncedFetch } from '@/composables/useDebouncedFetch'
import { searchProducts } from '@/api'

const cartStore = useCartStore()

const { data: products, loading, execute: search } = useDebouncedFetch(searchProducts)

const filteredProducts = computed(() => {
  const { start, end } = pagination.paginatedRange.value
  return products.value?.slice(start, end) ?? []
})

const pagination = usePagination({
  total: () => products.value?.length ?? 0,
  pageSize: 12,
})

function onSearch(keyword: string) {
  search(keyword)
  pagination.goToPage(1)
}

function addToCart(product: Product) {
  cartStore.addItem({
    id: product.id,
    name: product.name,
    price: product.price,
  })
}
</script>

这个模式中:购物车状态跨页面共享,用Pinia;搜索分页逻辑组件内闭环,用Composable。数据流向一目了然。

Pinia Store间跨模块通信

Store之间可以互相引用,实现跨模块状态联动。比如购物车总价需要在订单Store中使用:

// stores/order.ts
import { defineStore } from 'pinia'
import { useCartStore } from './cart'

export const useOrderStore = defineStore('order', () => {
  const cartStore = useCartStore()

  const orderSummary = computed(() => ({
    items: cartStore.items,
    total: cartStore.totalPrice,
    count: cartStore.totalCount,
    shipping: cartStore.totalPrice > 99 ? 0 : 10,
    finalPrice: cartStore.totalPrice + (cartStore.totalPrice > 99 ? 0 : 10),
  }))

  async function submitOrder() {
    if (cartStore.items.length === 0) {
      throw new Error('购物车为空')
    }
    return api.createOrder({
      items: cartStore.items,
      total: orderSummary.value.finalPrice,
    })
  }

  return { orderSummary, submitOrder }
})

Store间引用的注意事项:避免循环依赖。如果两个Store互相import,Pinia会在首次使用时才初始化,不会报错,但代码可读性差。更好的做法是将共享逻辑提取为独立的工具函数。

Pinia持久化与SSR适配

浏览器环境下,用户刷新页面后Pinia状态丢失。用pinia-plugin-persistedstate自动持久化到localStorage:

// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

// Store中启用
export const useUserStore = defineStore('user', () => {
  // ...
}, {
  persist: {
    key: 'yunthe-user',
    paths: ['token', 'userInfo'],
    storage: localStorage,
  }
})

SSR场景下localStorage不可用,需要区分客户端和服务端:

export const useUserStore = defineStore('user', () => {
  // ...
}, {
  persist: {
    key: 'yunthe-user',
    paths: ['token', 'userInfo'],
    storage: import.meta.env.SSR ? undefined : localStorage,
  }
})

Pinia负责全局状态的存储和共享,Composable负责组件级逻辑的封装和复用——两者搭配使用才能发挥Vue3组合式API的最大价值。数据流从Composable向Store汇聚,Store向下分发,单向清晰可追踪。

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

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

相关推荐