Vue3组合式API状态管理模式:从Pinia到自定义Composable的进阶实践

为什么Vue3项目需要重新审视状态管理

Vue3的组合式API(Composition API)从根本上改变了状态组织方式。ref和reactive提供了组件内部的响应式状态,Pinia作为官方状态管理库处理跨组件状态共享。但实际项目中,大量状态既不需要全局共享,又不适合散落在组件内部——它们需要在一个功能模块的多个组件间共享,且随组件卸载而清理。这正是自定义Composable发挥作用的场景。

Pinia Store设计的常见误区与修正

Pinia的扁平化Store设计比Vuex的模块系统更灵活,但也容易滥用。三个常见问题:

问题1:把所有状态塞进一个Store

// 错误:巨型Store
export const useAppStore = defineStore('app', () => {
  const user = ref(null)
  const token = ref('')
  const products = ref([])
  const cart = ref([])
  const orders = ref([])
  const theme = ref('light')
  const notifications = ref([])
  // ...50个状态和30个action
})

// 正确:按领域拆分Store
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const token = useStorage('token', '')
  const isLoggedIn = computed(() => !!token.value)
  return { user, token, isLoggedIn }
})

export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  const count = computed(() => items.value.length)
  const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0))
  return { items, count, total }
})

问题2:Store中直接操作DOM或路由

// 错误:Store中不应有副作用
export const useUserStore = defineStore('user', () => {
  const login = async (credentials) => {
    const res = await api.login(credentials)
    token.value = res.token
    router.push('/dashboard')  // 副作用!路由跳转不应在Store中
  }
})

// 正确:Store只管状态,副作用在组件或中间件中处理
export const useUserStore = defineStore('user', () => {
  const login = async (credentials) => {
    const res = await api.login(credentials)
    token.value = res.token
    return res  // 让调用方决定跳转
  }
})

// 组件中
const handleLogin = async () => {
  await userStore.login(form)
  router.push('/dashboard')
}

自定义Composable:模块级状态共享的最佳方案

Composable函数的命名约定以use开头,返回响应式状态和方法。模块级状态共享的实现方式:

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

// 模块级共享状态(在同一模块的多个组件间共享)
const products = ref([])
const loading = ref(false)
const error = ref(null)
let initialized = false
let activeConsumers = 0

export function useProductList() {
  const filteredProducts = computed(() => {
    return products.value.filter(p => p.status === 'active')
  })

  const fetchProducts = async () => {
    if (initialized && products.value.length > 0) return  // 避免重复请求
    loading.value = true
    error.value = null
    try {
      const res = await fetch('/api/products')
      products.value = await res.json()
      initialized = true
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }

  // 生命周期管理:最后一个消费者卸载时清理状态
  activeConsumers++
  onScopeDispose(() => {
    activeConsumers--
    if (activeConsumers <= 0) {
      products.value = []
      initialized = false
      activeConsumers = 0
    }
  })

  return {
    products: filteredProducts,
    allProducts: products,
    loading,
    error,
    fetchProducts
  }
}

Pinia + Composable的分层架构

生产项目的推荐分层方式:全局状态(用户信息、权限、主题)放Pinia Store,功能模块状态(商品列表、搜索条件、分页)放Composable,组件私有状态(表单输入、UI开关)放组件ref。

// 分层示例:商品搜索页
// 全局层 - Pinia Store
const userStore = useUserStore()  // 用户信息、权限

// 模块层 - Composable
const { products, loading, fetchProducts } = useProductList()
const { filters, filtered, applyFilter } = useProductFilter(products)

// 组件层 - ref
const currentPage = ref(1)
const pageSize = ref(20)
const showFilterPanel = ref(false)

异步状态管理的防抖与缓存

Composable中处理异步状态时,竞态条件和重复请求是两大痛点:

// composables/useAsyncState.ts - 通用异步状态Composable
import { ref, shallowRef } from 'vue'

export function useAsyncState(fn, options = {}) {
  const { debounceMs = 0, cacheMs = 0 } = options
  const data = shallowRef(null)
  const error = ref(null)
  const isLoading = ref(false)
  let lastFetchTime = 0
  let pendingRequest = null
  let debounceTimer = null

  const execute = async (...args) => {
    // 缓存检查
    if (cacheMs > 0 && Date.now() - lastFetchTime < cacheMs && data.value) {
      return data.value
    }

    // 取消上一次未完成的请求(防竞态)
    if (pendingRequest) {
      pendingRequest.aborted = true
    }
    const currentRequest = { aborted: false }
    pendingRequest = currentRequest

    // 防抖
    if (debounceMs > 0) {
      return new Promise((resolve) => {
        clearTimeout(debounceTimer)
        debounceTimer = setTimeout(async () => {
          const result = await _execute(currentRequest, args)
          resolve(result)
        }, debounceMs)
      })
    }

    return _execute(currentRequest, args)
  }

  const _execute = async (request, args) => {
    isLoading.value = true
    error.value = null
    try {
      const result = await fn(...args)
      if (!request.aborted) {
        data.value = result
        lastFetchTime = Date.now()
      }
      return result
    } catch (e) {
      if (!request.aborted) {
        error.value = e
      }
      throw e
    } finally {
      if (!request.aborted) {
        isLoading.value = false
        pendingRequest = null
      }
    }
  }

  return { data, error, isLoading, execute }
}

// 使用示例
const { data: searchResults, isLoading, execute: search } = useAsyncState(
  (query) => fetch('/api/search?q=' + query).then(r => r.json()),
  { debounceMs: 300, cacheMs: 60000 }
)

SSR场景下的状态序列化

Nuxt3或手动SSR场景中,服务端的状态需要序列化传递到客户端。Pinia通过pinia.state.value自动处理,但自定义Composable需要手动处理:

// SSR状态注入与提取
// server端
const pinia = createPinia()
// ...执行数据获取
const state = JSON.stringify(pinia.state.value)
// 注入HTML
// <script>window.__PINIA_STATE__ = ${state}</script>

// client端
const pinia = createPinia()
if (window.__PINIA_STATE__) {
  pinia.state.value = window.__PINIA_STATE__
  delete window.__PINIA_STATE__
}

自定义Composable的SSR状态传递需借助useState(Nuxt3提供)或将状态提升到Pinia Store中管理。核心原则:服务端渲染期间的状态必须在组件setup阶段同步获取,异步操作需要用useAsyncData或useFetch包裹。

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

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

相关推荐