Pinia 渐进式学习指南


目录

  1. 什么是 Pinia?
  2. 安装与初始化
  3. 第一个 Store
  4. State(状态)
  5. Getters(计算属性)
  6. Actions(方法)
  7. 在组件中使用 Store
  8. Store 之间的组合
  9. 插件机制
  10. 测试 Store
  11. 最佳实践总结

一、什么是 Pinia?

Pinia 是 Vue 官方推荐的状态管理库,也是 Vuex 的继任者。它具有以下特点:

  • 更简洁的 API,无需 mutations,直接修改状态
  • 天然支持 TypeScript,类型推断开箱即用
  • 支持 Composition API,与 Vue 3 无缝集成
  • 模块化设计,每个 Store 都是独立的,没有嵌套模块
  • 集成 Vue DevTools,支持调试

与 Vuex 对比:

特性VuexPinia
Mutations必须不需要
模块嵌套嵌套模块扁平独立 Store
TypeScript需要额外配置原生支持
Composition API不原生支持完整支持
代码量较多精简

二、安装与初始化

安装

npm install pinia
# 或
pnpm add pinia

在 Vue 应用中挂载

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'const app = createApp(App)
const pinia = createPinia()
​
app.use(pinia)
app.mount('#app')

推荐目录结构

src/
  stores/
    counter.ts     # 计数器 Store
    user.ts        # 用户 Store
    cart.ts        # 购物车 Store
  components/
  App.vue
  main.ts

三、第一个 Store

Store 的两种写法

Pinia 提供两种风格,推荐使用 Setup Store(Composition API 风格)。

Option Store(熟悉 Vue Options API 的人友好)

类比:statedatagetterscomputedactionsmethods

// stores/counter.ts
import { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Pinia',
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})
Setup Store(推荐)

和写 <script setup> 一模一样的体验:

// stores/counter.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', () => {
  // ref() → state
  const count = ref(0)
  const name = ref('Pinia')
​
  // computed() → getters
  const doubleCount = computed(() => count.value * 2)
​
  // function() → actions
  function increment() {
    count.value++
  }
​
  // 必须 return 所有需要被追踪的属性
  return { count, name, doubleCount, increment }
})

四、State(状态)

基本使用

// stores/user.ts
import { defineStore } from 'pinia'interface UserInfo {
  id: number
  name: string
  email: string
}
​
export const useUserStore = defineStore('user', {
  state: () => ({
    userList: [] as UserInfo[],
    currentUser: null as UserInfo | null,
    isLoading: false,
  }),
})

直接修改 state

const store = useUserStore()
​
// 直接赋值(Pinia 允许直接修改,无需 mutations)
store.isLoading = true
store.currentUser = { id: 1, name: 'Alice', email: 'alice@example.com' }

批量修改:$patch

当需要同时修改多个属性时,用 $patch 更高效(只触发一次响应):

// 对象语法
store.$patch({
  isLoading: false,
  currentUser: { id: 1, name: 'Alice', email: 'alice@example.com' },
})
​
// 函数语法(适合复杂操作,如数组 push)
store.$patch((state) => {
  state.userList.push({ id: 2, name: 'Bob', email: 'bob@example.com' })
  state.isLoading = false
})

重置状态:$reset

Option Store 内置 $reset()

store.$reset() // 恢复到初始状态

Setup Store 需要手动实现:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  function $reset() {
    count.value = 0
  }

  return { count, $reset }
})

状态变化:$subscribe

// 类似 watch,但专为 Store 设计
store.$subscribe((mutation, state) => {
  // mutation.type: 'direct' | 'patch object' | 'patch function'
  // mutation.storeId: Store 的 id
  console.log(`[${mutation.type}] storeId: ${mutation.storeId}`)

  // 将状态持久化到 localStorage
  localStorage.setItem('user', JSON.stringify(state))
})

// 组件卸载后仍然保持订阅(默认会随组件卸载而销毁)
store.$subscribe(callback, { detached: true })

五、Getters(计算属性)

Getters 等同于 Vue 的 computed,会自动缓存结果。

基础用法

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as { name: string; price: number; qty: number }[],
  }),
  getters: {
    // 参数 state,自动推断类型
    totalPrice: (state) =>
      state.items.reduce((sum, item) => sum + item.price * item.qty, 0),

    // 访问其他 getter,需要用 this(需手动标注返回类型)
    formattedTotal(): string {
      return ${this.totalPrice.toFixed(2)}`
    },
  },
})

带参数的 Getter

Getter 返回一个函数(注意:带参数的 Getter 不会缓存):

getters: {
  getItemByName: (state) => {
    return (name: string) => state.items.find((item) => item.name === name)
  },

  // 手动缓存:先过滤再查找
  getActiveItem(state) {
    const activeItems = state.items.filter((i) => i.qty > 0) // 缓存这部分
    return (name: string) => activeItems.find((i) => i.name === name)
  },
},
<script setup>
const cart = useCartStore()
const apple = cart.getItemByName('苹果')
</script>

跨 Store 访问

import { useUserStore } from './user'

getters: {
  personalizedGreeting(state) {
    const userStore = useUserStore()
    return `你好,${userStore.currentUser?.name}!你有 ${state.items.length} 件商品`
  },
},

六、Actions(方法)

Actions 既可以是同步的,也可以是异步的,直接替代了 Vuex 中的 mutations + actions 两层结构。

同步 Action

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++
    },
    incrementBy(amount: number) {
      this.count += amount
    },
  },
})

异步 Action(最常用)

export const useUserStore = defineStore('user', {
  state: () => ({
    currentUser: null as UserInfo | null,
    isLoading: false,
    error: null as string | null,
  }),
  actions: {
    async fetchUser(userId: number) {
      this.isLoading = true
      this.error = null
      try {
        const response = await fetch(`/api/users/${userId}`)
        if (!response.ok) throw new Error('请求失败')
        this.currentUser = await response.json()
      }
      catch (e) {
        this.error = (e as Error).message
      }
      finally {
        this.isLoading = false
      }
    },
  },
})

跨 Store 调用

import { useAuthStore } from './auth'

actions: {
  async loadProfile() {
    const auth = useAuthStore()
    if (!auth.isLoggedIn) return

    // SSR 注意:在 await 之前调用所有 useStore()
    this.profile = await fetchProfile(auth.userId)
  },
},

Action 调用:$onAction

const unsubscribe = store.$onAction(({ name, args, after, onError }) => {
  console.log(`调用了 action: ${name},参数:`, args)

  after((result) => {
    console.log(`${name} 执行成功,结果:`, result)
  })

  onError((error) => {
    console.error(`${name} 执行失败:`, error)
  })
})

// 手动取消订阅
unsubscribe()

// 组件卸载后仍保持订阅
store.$onAction(callback, true)

七、在组件中使用 Store

基本使用

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
</script>

<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Double: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">+1</button>
  </div>
</template>

解构时保持响应性:storeToRefs

直接解构 store 会丢失响应性,必须用 storeToRefs

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

//  错误:count 和 doubleCount 失去响应性
const { count, doubleCount } = counter

//  正确:state 和 getter 用 storeToRefs 解构
const { count, doubleCount } = storeToRefs(counter)

//  正确:actions 可以直接解构(它们不是响应式数据)
const { increment } = counter
</script>

在模板中直接绑定 Store 状态

<template>
  <!-- v-model 直接绑定 store 中的状态 -->
  <input v-model="userStore.searchKeyword" placeholder="搜索..." />
</template>

Options API 中使用(兼容写法)

import { mapState, mapWritableState, mapActions } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default {
  computed: {
    // 只读映射(state + getters)
    ...mapState(useCounterStore, ['count', 'doubleCount']),
    // 可写映射(state)
    ...mapWritableState(useCounterStore, ['count']),
  },
  methods: {
    ...mapActions(useCounterStore, ['increment']),
  },
}

八、Store 之间的组合

单向依赖

合理拆分 Store,通过在 action 或 getter 中引用其他 store:

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

export const useOrderStore = defineStore('order', {
  state: () => ({
    orders: [] as Order[],
  }),
  actions: {
    async submitOrder() {
      const cart = useCartStore()
      const user = useUserStore()

      const order = await api.createOrder({
        userId: user.currentUser!.id,
        items: cart.items,
        total: cart.totalPrice,
      })

      this.orders.push(order)
      cart.$reset() // 下单后清空购物车
    },
  },
})

避免循环依赖

如果两个 Store 互相引用,可以将共享逻辑提取到第三个 Store 或普通的 composable 中:

 userStore → authStore → userStore  (循环)

 authStore → userStore              (单向)
 sessionComposable(普通函数,不是 Store)

九、插件机制

插件可以为所有 Store 添加全局属性、方法或订阅行为。

创建插件

// plugins/persistPlugin.ts
import type { PiniaPlugin } from 'pinia'

export const persistPlugin: PiniaPlugin = ({ store }) => {
  // 从 localStorage 恢复状态
  const saved = localStorage.getItem(store.$id)
  if (saved) {
    store.$patch(JSON.parse(saved))
  }

  // 变化,自动持久化
  store.$subscribe((_mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

注册插件

// main.ts
import { createPinia } from 'pinia'
import { persistPlugin } from './plugins/persistPlugin'

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

为插件属性添加 TypeScript 类型

// 扩展 PiniaCustomProperties 接口
declare module 'pinia' {
  export interface PiniaCustomProperties {
    $persistedAt: Date
  }
}

十、测试 Store

安装测试工具

npm install -D @pinia/testing vitest

单独测试 Store 逻辑

// stores/counter.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from './counter'

describe('useCounterStore', () => {
  beforeEach(() => {
    // 每次测试前创建新的 Pinia 实例,避免测试间状态污染
    setActivePinia(createPinia())
  })

  it('初始 count 为 0', () => {
    const store = useCounterStore()
    expect(store.count).toBe(0)
  })

  it('increment 使 count 加 1', () => {
    const store = useCounterStore()
    store.increment()
    expect(store.count).toBe(1)
  })

  it('doubleCount 是 count 的两倍', () => {
    const store = useCounterStore()
    store.count = 5
    expect(store.doubleCount).toBe(10)
  })
})

在组件测试中 Mock Store

// components/Counter.test.ts
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { vi } from 'vitest'
import Counter from './Counter.vue'
import { useCounterStore } from '@/stores/counter'

it('点击按钮调用 increment', async () => {
  const wrapper = mount(Counter, {
    global: {
      plugins: [
        createTestingPinia({
          // Actions 默认被替换为 spy(不真正执行)
          createSpy: vi.fn,
          initialState: {
            counter: { count: 10 }, // 设置初始状态
          },
        }),
      ],
    },
  })

  const store = useCounterStore()

  await wrapper.find('button').trigger('click')

  // 验证 action 被调用
  expect(store.increment).toHaveBeenCalledOnce()
})

十一、最佳实践总结

Store 设计原则

 按业务领域划分 Store(user、cart、product)
 每个 Store 职责单一,不要把所有状态塞到一个 Store
 优先使用 Setup Store(更灵活,可使用 composables 和 watchers)
 Store 只存放全局共享状态,局部组件状态用 ref/reactive

响应性规范

 解构 state/getter 时使用 storeToRefs()
 actions 可以直接解构,无需 storeToRefs
 批量修改状态时用 $patch,减少响应触发次数

性能与安全

 在 SSR 中,await 之前提前调用所有 useStore()
 避免 Store 之间循环引用
 不要在模块顶层(组件外)调用 useStore(),应在函数内部调用
 敏感数据(token、密钥)不要存入 Store,避免 DevTools 泄露

快速查阅

需求方案
修改单个属性store.count = 1
修改多个属性store.$patch({ ... })
复杂修改(数组等)store.$patch(state => { ... })
重置到初始状态store.$reset()
订阅状态变化store.$subscribe(cb)
订阅 Action 调用store.$onAction(cb)
解构保持响应性storeToRefs(store)

进阶资源

  • Pinia 官方文档
  • Pinia GitHub
  • pinia-plugin-persistedstate(状态持久化)
  • VueUse(可在 Setup Store 中使用的组合式函数库)
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com