Vue3组合式API状态管理:Pinia与Composable实战模式

从Vuex到Pinia的迁移逻辑

Vue3项目中的状态管理方案,Pinia已成为事实标准。相比Vuex,Pinia去掉了mutation层、简化了模块注册、内置TypeScript推断、支持Composition API写法,代码量减少约40%。迁移成本不高,核心思路是把Vuex的state/computed映射为Pinia的state/getters,把actions直接平移。

Pinia的Store定义方式有两种:Option API风格和Setup风格。Setup风格和Composition API一致,灵活度更高,推荐在新项目中优先使用。

Setup Store的标准写法

以用户认证Store为例:

import { defineStore } from "pinia"

import { ref, computed } from "vue"

import { loginApi, getUserInfoApi } from "@/api/auth"

export const useAuthStore = defineStore("auth", () => {

const token = ref(localStorage.getItem("token") || "")

const userInfo = ref(null)

const isLoggedIn = computed(() => !!token.value)

const userName = computed(() => userInfo.value?.name ?? "")

async function login(credentials) {

const res = await loginApi(credentials)

token.value = res.token

localStorage.setItem("token", res.token)

await fetchUserInfo()

}

async function fetchUserInfo() {

userInfo.value = await getUserInfoApi()

}

function logout() {

token.value = ""

userInfo.value = null

localStorage.removeItem("token")

}

return { token, userInfo, isLoggedIn, userName, login, fetchUserInfo, logout }

})

ref声明的变量即为state,computed即为getters,普通函数即为actions。结构清晰,无需额外记忆mutation/action区别。

Composable模式处理局部状态

并非所有状态都需要放进全局Store。组件级别的可复用逻辑用Composable封装更合适。一个常见的判断标准:如果状态只在两个以上组件间共享且跨路由持久化,放Pinia;如果只在同一页面的多个组件间共享,用Composable。

以表格分页逻辑为例,封装为Composable:

import { ref, computed, watch } from "vue"

export function usePagination(fetchFn, pageSize = 20) {

const currentPage = ref(1)

const total = ref(0)

const data = ref([])

const loading = ref(false)

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

async function loadData() {

loading.value = true

try {

const res = await fetchFn({ page: currentPage.value, size: pageSize })

data.value = res.list

total.value = res.total

} finally {

loading.value = false

}

}

function goToPage(page) {

if (page < 1 || page > totalPages.value) return

currentPage.value = page

}

watch(currentPage, loadData)

return { currentPage, total, data, loading, totalPages, loadData, goToPage }

}

组件中使用:

const { data, loading, currentPage, totalPages, goToPage } = usePagination(

(params) => api.getOrders(params),

20

)

Pinia插件扩展能力

Pinia的插件机制能实现跨切面逻辑。最典型的是持久化插件:

import { PiniaPluginContext } from "pinia"

function persistPlugin({ store }: PiniaPluginContext) {

const key = `pinia-${store.$id}`

const saved = localStorage.getItem(key)

if (saved) store.$patch(JSON.parse(saved))

store.$subscribe((_, state) => {

localStorage.setItem(key, JSON.stringify(state))

})

}

注册插件:

const pinia = createPinia()

pinia.use(persistPlugin)

也可以用现成的pinia-plugin-persistedstate包,功能更完善,支持sessionStorage和自定义序列化。

性能优化与常见陷阱

Pinia的store响应式基于Vue的reactive系统。一个常见性能问题是Store中存放了大型对象数组,每次修改触发组件全量重渲染。解决方案是用shallowRef替代ref存放大数据:

const bigList = shallowRef([])

// 更新时整体替换引用而非逐项修改

bigList.value = [...newList]

另一个陷阱是在Composable中直接解构Store的reactive属性。解构会丢失响应性,必须用storeToRefs:

const authStore = useAuthStore()

// 错误写法:isLoggedIn变成普通布尔值,失去响应性

const { isLoggedIn } = authStore

// 正确写法

const { isLoggedIn } = storeToRefs(authStore)

action可以直接解构,因为函数本身不需要响应式追踪:

const { login, logout } = authStore // 正确

Pinia配合Vue3 DevTools能查看Store的状态时间线,调试比Vuex方便不少。大型项目中建议按业务域拆分Store,每个Store职责单一,不要出现一个”global”Store包揽全部状态的情况。

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

(0)
小编小编
上一篇 2026年8月6日
下一篇 2026年8月6日

相关推荐

Vue3组合式API状态管理:Pinia与Composable的分层架构设计

Pinia不是Vuex的简单替代品

Vue3生态中,Pinia取代Vuex成为官方推荐的状态管理方案,这不是换个API写法的事。Pinia去掉了Vuex的mutation概念,支持Composition API风格定义store,天然支持TypeScript类型推导,且支持多个store实例独立运行。但对于中大型前端项目,光有Pinia不够——Composable函数承担了组件逻辑复用的职责,和Pinia的职责边界需要明确划分,否则代码会陷入什么都往store里塞或到处都是composable的两个极端。

前端工程化的核心挑战之一就是状态管理架构的分层设计。合理的分层让代码定位有规律可循,组件只关心视图逻辑。

三层架构:Component到Composable到Store

这三层的职责划分:

Component:纯视图逻辑,模板渲染、用户交互事件、样式计算

Composable:业务逻辑封装,API请求编排、数据转换、跨组件状态协调

Store:全局共享状态,跨页面持久数据、用户会话信息、全局配置

判断标准:状态是否需要跨路由页面共享?需要则Store;只跨组件共享则Composable;只在当前组件内则ref/reactive留在Component。

Pinia Store的设计规范

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

export const useUserStore = defineStore('user', () => {
  const userInfo = ref<UserInfo | null>(null)
  const token = ref<string>('')
  const permissions = ref<string[]>([])
  
  const isLoggedIn = computed(() => !!token.value)
  const hasPermission = computed(() => {
    return (perm: string) => permissions.value.includes(perm)
  })
  
  async function login(params: LoginParams) {
    const { data } = await userApi.login(params)
    token.value = data.token
    userInfo.value = data.user
    permissions.value = data.permissions
    localStorage.setItem('token', data.token)
  }
  
  async function logout() {
    await userApi.logout()
    $reset()
    localStorage.removeItem('token')
  }
  
  function $reset() {
    userInfo.value = null
    token.value = ''
    permissions.value = []
  }
  
  return { userInfo, token, permissions, isLoggedIn, hasPermission, login, logout, $reset }
})

Composable函数封装业务逻辑

// composables/useUserList.ts
import { ref, reactive } from 'vue'
import { userApi } from '@/api/user'

export function useUserList() {
  const loading = ref(false)
  const list = ref([])
  const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
  const searchParams = reactive({ keyword: '', status: undefined })
  
  async function fetchList() {
    loading.value = true
    try {
      const { data } = await userApi.getList({
        page: pagination.page, pageSize: pagination.pageSize,
        ...searchParams
      })
      list.value = data.list
      pagination.total = data.total
    } finally { loading.value = false }
  }
  
  function handlePageChange(page) { pagination.page = page; fetchList() }
  function handleSearch() { pagination.page = 1; fetchList() }
  
  fetchList()
  return { loading, list, pagination, searchParams, fetchList, handlePageChange, handleSearch }
}

组件中的调用方式

<script setup lang="ts">
import { useUserList } from '@/composables/useUserList'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const { loading, list, pagination, searchParams, handlePageChange, handleSearch } = useUserList()
</script>

组件代码极简——只做数据绑定和事件传递,业务逻辑全部在composable中,全局状态在store中。

跨端小程序开发的适配策略

uni-app + Vue3场景下,Pinia和Composable的架构同样适用,但需要注意小程序环境的限制:

// composables/useMiniAppAuth.ts
import { ref } from 'vue'
import { useUserStore } from '@/stores/user'

export function useMiniAppAuth() {
  const userStore = useUserStore()
  const authorizing = ref(false)
  
  async function authorize() {
    authorizing.value = true
    try {
      // #ifdef MP-WEIXIN
      const { code } = await uni.login({ provider: 'weixin' })
      await userStore.loginByCode(code)
      // #endif
      
      // #ifdef H5
      const redirect = encodeURIComponent(window.location.href)
      window.location.href = '/auth/wechat?redirect=' + redirect
      // #endif
    } finally { authorizing.value = false }
  }
  
  return { authorizing, authorize }
}

TypeScript类型安全与Store间协作

// stores/app.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { usePermissionStore } from './permission'

export const useAppStore = defineStore('app', () => {
  const initialized = ref(false)
  
  async function initApp() {
    const userStore = useUserStore()
    const permissionStore = usePermissionStore()
    
    const token = localStorage.getItem('token')
    if (!token) { initialized.value = true; return }
    
    userStore.token = token
    await permissionStore.fetchRoutes()
    initialized.value = true
  }
  
  return { initialized, initApp }
})

Store之间通过函数内部useXxxStore()调用实现依赖,避免模块级别的循环引用。

Web性能优化:Pinia的响应式追踪开销

大型项目中store字段过多时,Pinia的响应式追踪会产生不必要的计算开销:

storeToRefs按需解构:只解构组件需要的字段,避免整个store的响应式追踪

shallowRef处理大对象:列表数据不需要深层响应式时用shallowRef

computed缓存:派生状态用computed而非方法调用

// 优化前:整store响应式追踪
const userStore = useUserStore()

// 优化后:只追踪用到的字段
const { userInfo, isLoggedIn } = storeToRefs(useUserStore())

// 大列表用shallowRef
const bigList = shallowRef([])
bigList.value = await fetchBigList()

这套分层架构在多个Vue3项目中落地验证,10万行级别的前端代码库依然能保持清晰的模块边界和可维护性。组件文件平均不超过80行,composable函数职责单一,store只管全局共享数据。

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

(0)
小编小编
上一篇 2026年8月4日
下一篇 2026年8月4日

相关推荐