Vue3组合式API实战:Pinia状态管理与组合式函数设计模式

Vue3组合式API将组件逻辑组织方式从Options API的选项分散模式转变为函数聚合模式。Pinia作为Vue3官方状态管理库,天然契合组合式API风格。本文通过组合式函数封装业务逻辑、Pinia构建跨组件状态、持久化方案与测试,给出完整的工程化实践。

Pinia状态仓库搭建与模块化拆分

Pinia的Store使用defineStore定义,组合式API写法(Setup Store)直接返回响应式数据和方法,比Options写法更灵活。

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
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)
  })
  const displayName = computed(() => {
    if (!userInfo.value) return '游客'
    return userInfo.value.nickname || userInfo.value.username
  })

  async function login(credentials: LoginParams) {
    const res = await userApi.login(credentials)
    token.value = res.token
    userInfo.value = res.user
    permissions.value = res.permissions
    return res
  }

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

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

大型项目按业务域拆分Store,通过组合式函数在Store之间共享逻辑:

// composables/useEntityCrud.ts
import { ref, reactive } from 'vue'

export function useEntityCrud<T extends { id: string | number }>(
  fetchFn: (params: PageParams) => Promise<PageResult<T>>,
  createFn: (data: Partial<T>) => Promise<T>,
  updateFn: (id: T['id'], data: Partial<T>) => Promise<T>,
  deleteFn: (id: T['id']) => Promise<void>
) {
  const list = ref<T[]>([])
  const total = ref(0)
  const loading = ref(false)
  const params = reactive<PageParams>({
    page: 1, pageSize: 20, keyword: ''
  })

  async function fetchList() {
    loading.value = true
    try {
      const res = await fetchFn(params)
      list.value = res.items
      total.value = res.total
    } finally {
      loading.value = false
    }
  }

  async function create(data: Partial<T>) {
    await createFn(data)
    await fetchList()
  }

  async function update(id: T['id'], data: Partial<T>) {
    await updateFn(id, data)
    await fetchList()
  }

  async function remove(id: T['id']) {
    await deleteFn(id)
    await fetchList()
  }

  return { list, total, loading, params, fetchList, create, update, remove }
}

// 在Store中使用
export const useProductStore = defineStore('product', () => {
  const crud = useEntityCrud<Product>(
    productApi.list, productApi.create,
    productApi.update, productApi.delete
  )
  return { ...crud }
})

组合式函数封装业务逻辑

组合式函数是Vue3逻辑复用的核心机制。命名约定为use开头,内部使用响应式API,返回响应式数据和方法。与Mixin相比,组合式函数具有明确的类型推导和来源追踪。

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

export function usePagination(options: {
  total: () => number
  pageSize?: number
  onChange?: (page: number) => void
}) {
  const currentPage = ref(1)
  const pageSize = ref(options.pageSize ?? 20)
  const totalPages = computed(() =>
    Math.ceil(options.total() / pageSize.value)
  )

  function goTo(page: number) {
    if (page < 1 || page > totalPages.value) return
    currentPage.value = page
    options.onChange?.(page)
  }

  function next() { goTo(currentPage.value + 1) }
  function prev() { goTo(currentPage.value - 1) }
  function reset() { goTo(1) }

  return { currentPage, pageSize, totalPages, goTo, next, prev, reset }
}

组件中使用:

<script setup lang="ts">
import { useProductStore } from '@/stores/product'
import { usePagination } from '@/composables/usePagination'

const store = useProductStore()
store.fetchList()

const pagination = usePagination({
  total: () => store.total,
  onChange: (page) => {
    store.params.page = page
    store.fetchList()
  }
})
</script>

<template>
  <div v-for="item in store.list" :key="item.id">{{ item.name }}</div>
  <button @click="pagination.prev()" :disabled="pagination.currentPage === 1">上一页</button>
  {{ pagination.currentPage }} / {{ pagination.totalPages }}
  <button @click="pagination.next()" :disabled="pagination.currentPage === pagination.totalPages">下一页</button>
</template>

Pinia持久化与跨组件状态同步

使用pinia-plugin-persistedstate插件实现状态持久化:

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

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

// Store中声明持久化配置
export const useUserStore = defineStore('user', () => {
  const token = ref('')
  const userInfo = ref(null)
  return { token, userInfo }
}, {
  persist: {
    key: 'app-user',
    storage: localStorage,
    pick: ['token'],
  }
})

跨组件状态同步利用Pinia的响应式特性。多个组件引用同一个Store实例时,任一组件修改状态,依赖该状态的组件自动更新:

// Header.vue
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>
<template>
  <span>{{ userStore.displayName }}</span>
  <button @click="userStore.logout()">退出</button>
</template>

// Sidebar.vue — 根据权限渲染菜单
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const menus = [
  { label: '仪表盘', perm: 'dashboard:view' },
  { label: '用户管理', perm: 'user:manage' },
]
const visibleMenus = menus.filter(m => userStore.hasPermission(m.perm))
</script>

TypeScript类型推导与Store测试

Pinia的Setup Store天然支持TypeScript类型推导。测试时使用setActivePinia创建独立的Pinia实例:

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

vi.mock('@/api/user')

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

  it('登录后应设置token和用户信息', async () => {
    vi.mocked(userApi.login).mockResolvedValue({
      token: 'test-token',
      user: { id: 1, username: 'admin', nickname: '管理员' },
      permissions: ['dashboard:view', 'user:manage']
    })

    const store = useUserStore()
    await store.login({ username: 'admin', password: '123456' })

    expect(store.token).toBe('test-token')
    expect(store.isLoggedIn).toBe(true)
    expect(store.displayName).toBe('管理员')
    expect(store.hasPermission('user:manage')).toBe(true)
  })

  it('退出后应清空所有状态', () => {
    const store = useUserStore()
    store.token = 'some-token'
    store.logout()
    expect(store.token).toBe('')
    expect(store.isLoggedIn).toBe(false)
  })
})

组合式API配合Pinia的工程价值在于:逻辑以函数为单元拆分和复用,状态以Store为边界管理和持久化,类型在编译期完整推导,测试通过独立Pinia实例隔离副作用。这套模式在中大型Vue3项目中能显著降低组件复杂度,提升可维护性。

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

赞 (0)
小编小编
上一篇 2026年9月18日
下一篇 2026年9月18日

相关推荐

Vue3组合式API实战:Pinia状态管理与组件逻辑复用模式

Vue3的组合式API把状态与逻辑从”选项块”重新组织成”功能块”:同一业务的响应式数据、计算属性与方法聚合在一起,逻辑复用不再依赖mixin,解决了命名冲突与来源不清晰两大痛点。搭配官方状态库Pinia,中大型Vue3项目能实现类型安全的全局状态与可单元测试的业务逻辑。以下以购物车与用户登录态为例,覆盖从Store定义到性能优化的完整实现。

Options API与组合式API的核心差异

Options API按”数据、方法、生命周期”分块组织代码,一个功能的逻辑散落在多个选项里;组合式API按业务功能组织,相关逻辑集中维护。同一个计数功能对比:

// Options API
export default {
  data() { return { count: 0 } },
  methods: { inc() { this.count++ } },
  computed: { double() { return this.count * 2 } }
}

// 组合式API
import { ref, computed } from 'vue'
export default {
  setup() {
    const count = ref(0)
    const inc = () => count.value++
    const double = computed(() => count.value * 2)
    return { count, inc, double }
  }
}

功能简单时两种写法差别不大,组件复杂度上升后,组合式API”按功能聚合”的优势开始显现:查一个业务逻辑的所有相关代码,只需要读一段连续代码,而不是在data、methods、computed之间反复跳转。script setup语法糖进一步省去导出与return样板:

<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
</script>

Pinia状态管理:Store定义与持久化插件

Pinia没有mutations、没有嵌套模块,Store直接定义并支持完整的类型推断:

// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    coupon: null
  }),
  getters: {
    totalAmount: (state) =>
      state.items.reduce((sum, i) => sum + i.price * i.qty, 0),
    count: (state) => state.items.length
  },
  actions: {
    addItem(product, qty = 1) {
      const exist = this.items.find(i => i.id === product.id)
      if (exist) { exist.qty += qty } else { this.items.push({ ...product, qty }) }
    }
  }
})

组件中使用:

<script setup>
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const cart = useCartStore()
const { totalAmount } = storeToRefs(cart)  // 解构必须走storeToRefs保持响应性
</script>
<template>
  <span>{{ cart.count }}件 / {{ totalAmount.toFixed(2) }}元</span>
</template>

直接解构store实例会丢失响应性,必须通过storeToRefs包裹,这是Pinia使用中的最高频错误。登录态持久化交给插件完成:

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

// store内声明持久化配置,只持久化必要字段
// persist: { paths: ['coupon'] }

组合式函数:useXxx逻辑复用模式详解

跨组件复用逻辑时,把响应式状态与操作函数封装成组合式函数(Composable),取代mixin。一个带加载态的用户信息请求:

// composables/useUser.js
import { ref, onMounted } from 'vue'

export function useUser(userId) {
  const user = ref(null)
  const loading = ref(false)

  const fetchUser = async () => {
    loading.value = true
    try {
      const res = await fetch(`/api/users/${userId.value}`)
      user.value = await res.json()
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchUser)
  return { user, loading, refresh: fetchUser }
}

// 消费方
<script setup>
import { useUser } from '@/composables/useUser'
const { user, loading, refresh } = useUser(props.userId)
</script>

相比mixin,组合式函数的优势明确:来源清晰(import可见)、可传参、可组合、可单独做单元测试。命名约定统一为useXxx并集中放在composables目录,输入用ref或getter,返回普通对象内包含ref与函数。

组件通信方案对比与选型建议

三层结构的分工清晰:

  • props/emit:父子组件单向数据流,层级不超过3层时的默认选择。
  • provide/inject:跨层级配置透传与组件库内部共享,避免props逐层传递,配合readonly保证单向:
// 祖先组件
provide('theme', readonly(themeRef))
// 后代组件
const theme = inject('theme', ref('light'))  // 提供默认值防止注入缺失
  • Pinia:业务状态跨页面共享,登录态、购物车、字典数据等场景。把”仅UI状态”放进全局Store是常见反模式,全局状态越多,组件与Store的耦合越难拆。

性能优化:shallowRef、懒加载与大列表渲染

大列表与复杂对象场景下,浅层响应式能显著降低依赖追踪开销:

import { shallowRef, triggerRef } from 'vue'

// 万行表格数据:内部对象变更不追踪
const rows = shallowRef([])
rows.value = await fetchRows()      // 整体替换触发更新
// 单行原地修改需要手动触发
rows.value[0].selected = true
triggerRef(rows)

路由级与组件级懒加载减小首包体积:

const routes = [
  { path: '/report', component: () => import('@/views/Report.vue') }
]

// 重型图表组件按需引入
const HeavyChart = defineAsyncComponent(() =>
  import('@/components/HeavyChart.vue')
)

长列表渲染用虚拟滚动方案(vue-virtual-scroller)只渲染可视区域行。每轮优化完成后用Chrome Performance面板录制交互过程,对比脚本执行时间与帧率,避免没有度量依据的盲目优化。

组合式API配合Pinia与组合式函数,把Vue3项目的逻辑组织从”框架规定的结构”转变为”业务自然生长的结构”。新项目直接采用script setup加Pinia起步;存量项目迁移时优先抽取组合式函数,再逐步改造组件,避免一次性重写带来的回归风险。

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

赞 (0)
小编小编
上一篇 2026年9月14日
下一篇 2026年9月14日

相关推荐