Skip to content

Pinia 状态管理深入

Vue 官方推荐的状态管理库,轻量、类型安全、模块化

什么是 Pinia?

定义:Vue 的轻量级状态管理库,是 Vuex 的继任者。

核心特性

  • 轻量:约 1KB(gzip)
  • 类型安全:完整的 TypeScript 支持
  • 模块化:天然支持,无需嵌套
  • DevTools:时间旅行、热更新
  • 简洁:无 mutations,直接修改 state

一、基础使用

1.1 安装和配置

bash
npm install pinia
typescript
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

1.2 定义 Store

Setup 语法(推荐)

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

export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)
  const name = ref('Counter')

  // getters
  const doubleCount = computed(() => count.value * 2)

  // actions
  function increment() {
    count.value++
  }

  async function incrementAsync() {
    await new Promise(resolve => setTimeout(resolve, 1000))
    count.value++
  }

  return { count, name, doubleCount, increment, incrementAsync }
})

Options 语法

typescript
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter'
  }),

  getters: {
    doubleCount: (state) => state.count * 2
  },

  actions: {
    increment() {
      this.count++
    },

    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
    }
  }
})

1.3 使用 Store

vue
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const store = useCounterStore()

// ❌ 直接解构会失去响应式
const { count, doubleCount } = store

// ✅ 使用 storeToRefs 保持响应式
const { count, doubleCount } = storeToRefs(store)

// ✅ actions 可以直接解构
const { increment } = store
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

二、核心概念

2.1 State

定义 state

typescript
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const token = ref('')
  const permissions = ref<string[]>([])

  return { user, token, permissions }
})

访问 state

typescript
const store = useUserStore()

// 读取
console.log(store.user)

// 修改(直接修改)
store.user = { id: 1, name: 'Alice' }

// 批量修改
store.$patch({
  user: { id: 1, name: 'Alice' },
  token: 'abc123'
})

// 函数式修改
store.$patch((state) => {
  state.user = { id: 1, name: 'Alice' }
  state.permissions.push('admin')
})

// 重置
store.$reset()

2.2 Getters

定义 getters

typescript
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)

  // 简单 getter
  const isLoggedIn = computed(() => !!user.value)

  // 带参数的 getter
  const hasPermission = computed(() => {
    return (permission: string) => {
      return user.value?.permissions.includes(permission) ?? false
    }
  })

  // 访问其他 store 的 getter
  const cartTotal = computed(() => {
    const cartStore = useCartStore()
    return cartStore.total
  })

  return { user, isLoggedIn, hasPermission, cartTotal }
})

使用 getters

typescript
const store = useUserStore()

console.log(store.isLoggedIn) // 直接访问
console.log(store.hasPermission('admin')) // 带参数

2.3 Actions

定义 actions

typescript
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  // 同步 action
  function setUser(newUser: User) {
    user.value = newUser
  }

  // 异步 action
  async function login(credentials: Credentials) {
    loading.value = true
    error.value = null

    try {
      const response = await api.login(credentials)
      user.value = response.user
      return response
    } catch (e) {
      error.value = e as Error
      throw e
    } finally {
      loading.value = false
    }
  }

  // 调用其他 store 的 action
  async function logout() {
    const cartStore = useCartStore()
    await cartStore.clear()
    user.value = null
  }

  return { user, loading, error, setUser, login, logout }
})

三、Store 组合

3.1 Store 之间的引用

typescript
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const isLoggedIn = computed(() => !!user.value)

  return { user, isLoggedIn }
})

// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  const userStore = useUserStore() // 引用其他 store

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

  const total = computed(() => {
    // 只有登录用户才计算总价
    if (!userStore.isLoggedIn) return 0
    return items.value.reduce((sum, item) => sum + item.price, 0)
  })

  function addItem(item: CartItem) {
    if (!userStore.isLoggedIn) {
      throw new Error('Please login first')
    }
    items.value.push(item)
  }

  return { items, total, addItem }
})

3.2 组合式函数复用逻辑

typescript
// composables/useAsync.ts
export function useAsync<T>(asyncFn: () => Promise<T>) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  async function execute() {
    loading.value = true
    error.value = null

    try {
      data.value = await asyncFn()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  return { data, loading, error, execute }
}

// stores/product.ts
export const useProductStore = defineStore('product', () => {
  const products = ref<Product[]>([])

  // 复用异步逻辑
  const { loading, error, execute: fetchProducts } = useAsync(async () => {
    const response = await api.getProducts()
    products.value = response.data
    return response.data
  })

  return { products, loading, error, fetchProducts }
})

四、插件系统

4.1 持久化插件

typescript
// plugins/persist.ts
import { PiniaPluginContext } from 'pinia'

export function createPersistedState() {
  return ({ store }: PiniaPluginContext) => {
    const key = `pinia-${store.$id}`

    // 从 localStorage 恢复
    const saved = localStorage.getItem(key)
    if (saved) {
      store.$patch(JSON.parse(saved))
    }

    // 监听变化,保存到 localStorage
    store.$subscribe((mutation, state) => {
      localStorage.setItem(key, JSON.stringify(state))
    })
  }
}

// main.ts
const pinia = createPinia()
pinia.use(createPersistedState())

使用第三方插件

bash
npm install pinia-plugin-persistedstate
typescript
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  return { user }
}, {
  persist: true // 启用持久化
})

4.2 日志插件

typescript
export function createLogger() {
  return ({ store }: PiniaPluginContext) => {
    store.$subscribe((mutation, state) => {
      console.group(`[Pinia] ${mutation.storeId}`)
      console.log('Mutation:', mutation.type)
      console.log('Payload:', mutation.payload)
      console.log('State:', state)
      console.groupEnd()
    })

    store.$onAction(({ name, args, after, onError }) => {
      const startTime = Date.now()

      console.log(`[Action] ${store.$id}.${name}`, args)

      after((result) => {
        console.log(
          `[Action] ${store.$id}.${name} finished in ${Date.now() - startTime}ms`,
          result
        )
      })

      onError((error) => {
        console.error(`[Action] ${store.$id}.${name} failed`, error)
      })
    })
  }
}

五、高级用法

5.1 $subscribe 监听 state 变化

typescript
const store = useUserStore()

// 监听所有 state 变化
store.$subscribe((mutation, state) => {
  console.log(mutation.type) // 'direct' | 'patch object' | 'patch function'
  console.log(mutation.storeId) // 'user'
  console.log(mutation.payload) // 传递给 $patch 的对象
  console.log(state) // 当前 state
})

// 组件卸载后仍然监听
store.$subscribe(callback, { detached: true })

5.2 $onAction 监听 actions

typescript
const unsubscribe = store.$onAction(({
  name,      // action 名称
  store,     // store 实例
  args,      // action 参数
  after,     // action 成功后的钩子
  onError    // action 失败后的钩子
}) => {
  const startTime = Date.now()

  console.log(`Start "${name}" with params [${args.join(', ')}]`)

  after((result) => {
    console.log(
      `Finished "${name}" after ${Date.now() - startTime}ms with result:`,
      result
    )
  })

  onError((error) => {
    console.error(`Failed "${name}" after ${Date.now() - startTime}ms`, error)
  })
})

// 取消监听
unsubscribe()

5.3 动态 Store

typescript
// 动态创建 store
function createDynamicStore(id: string) {
  return defineStore(id, () => {
    const data = ref({})
    return { data }
  })
}

// 使用
const store1 = createDynamicStore('dynamic-1')()
const store2 = createDynamicStore('dynamic-2')()

六、TypeScript 支持

6.1 完整类型定义

typescript
interface User {
  id: number
  name: string
  email: string
  permissions: string[]
}

interface UserState {
  user: User | null
  token: string
  loading: boolean
  error: Error | null
}

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref<string>('')
  const loading = ref<boolean>(false)
  const error = ref<Error | null>(null)

  // Getters
  const isLoggedIn = computed<boolean>(() => !!user.value)
  const userName = computed<string>(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(email: string, password: string): Promise<User> {
    loading.value = true
    error.value = null

    try {
      const response = await api.login({ email, password })
      user.value = response.user
      token.value = response.token
      return response.user
    } catch (e) {
      error.value = e as Error
      throw e
    } finally {
      loading.value = false
    }
  }

  function logout(): void {
    user.value = null
    token.value = ''
  }

  return {
    user,
    token,
    loading,
    error,
    isLoggedIn,
    userName,
    login,
    logout
  }
})

// 类型推导
type UserStore = ReturnType<typeof useUserStore>

6.2 扩展 Store 类型

typescript
declare module 'pinia' {
  export interface PiniaCustomProperties {
    // 添加自定义属性
    $router: Router
    $api: Api
  }

  export interface PiniaCustomStateProperties<S> {
    // 添加自定义 state 属性
    $createdAt: Date
  }
}

// 在插件中使用
pinia.use(({ store }) => {
  store.$router = router
  store.$api = api
  store.$createdAt = new Date()
})

七、Pinia vs Vuex

7.1 核心区别

特性PiniaVuex
大小~1KB~2KB
TypeScript✅ 完整支持⚠️ 支持较弱
Mutations❌ 无需✅ 必需
模块化✅ 天然支持⚠️ 需要嵌套
DevTools✅ 支持✅ 支持
热更新✅ 支持✅ 支持
插件系统✅ 简洁✅ 复杂

7.2 代码对比

Pinia

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

  function increment() {
    count.value++ // 直接修改
  }

  return { count, increment }
})

// 使用
const store = useCounterStore()
store.count++ // 直接修改
store.increment() // 调用 action

Vuex

typescript
export default {
  state: () => ({
    count: 0
  }),

  mutations: {
    INCREMENT(state) {
      state.count++
    }
  },

  actions: {
    increment({ commit }) {
      commit('INCREMENT')
    }
  }
}

// 使用
store.commit('INCREMENT') // 必须通过 mutation
store.dispatch('increment') // 或通过 action

7.3 迁移指南

从 Vuex 迁移到 Pinia

typescript
// Vuex
const store = new Vuex.Store({
  state: { count: 0 },
  getters: {
    double: state => state.count * 2
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => commit('increment'), 1000)
    }
  }
})

// Pinia
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const double = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  async function incrementAsync() {
    await new Promise(resolve => setTimeout(resolve, 1000))
    increment()
  }

  return { count, double, increment, incrementAsync }
})

八、最佳实践

8.1 Store 组织

按功能模块拆分

stores/
├── user.ts          # 用户相关
├── cart.ts          # 购物车
├── product.ts       # 产品
├── order.ts         # 订单
└── index.ts         # 导出所有 stores

index.ts

typescript
export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useProductStore } from './product'
export { useOrderStore } from './order'

8.2 命名规范

typescript
// ✅ 推荐
export const useUserStore = defineStore('user', () => {})
export const useCartStore = defineStore('cart', () => {})

// ❌ 不推荐
export const userStore = defineStore('user', () => {})
export const UserStore = defineStore('user', () => {})

8.3 避免直接导出 store 实例

typescript
// ❌ 不推荐
export const userStore = useUserStore()

// ✅ 推荐
export const useUserStore = defineStore('user', () => {})

// 在组件中使用
const userStore = useUserStore()

8.4 使用 storeToRefs

typescript
// ❌ 失去响应式
const { count, doubleCount } = store

// ✅ 保持响应式
const { count, doubleCount } = storeToRefs(store)

// ✅ actions 可以直接解构
const { increment } = store

8.5 错误处理

typescript
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const error = ref<Error | null>(null)

  async function login(credentials: Credentials) {
    error.value = null

    try {
      const response = await api.login(credentials)
      user.value = response.user
      return response
    } catch (e) {
      error.value = e as Error
      // 可以在这里统一处理错误
      console.error('Login failed:', e)
      throw e // 重新抛出,让调用者处理
    }
  }

  return { user, error, login }
})

面试高频题

1: Pinia 和 Vuex 有什么区别?

答案

Pinia 优势

  1. 更轻量:约 1KB vs 2KB
  2. 无 mutations:直接修改 state,代码更简洁
  3. TypeScript 支持更好:完整的类型推导
  4. 模块化更简单:天然支持,无需嵌套
  5. Vue 3 原生支持:使用 Composition API

Vuex 劣势

  1. mutations 样板代码多
  2. 模块嵌套复杂
  3. TypeScript 支持弱

2: Pinia 的 Store 何时被创建?

答案

Store 是懒创建的,只有在第一次调用 useXxxStore() 时才会创建实例。

typescript
// 定义 store(不会创建实例)
export const useUserStore = defineStore('user', () => {})

// 第一次调用时创建实例
const store = useUserStore() // 创建

// 后续调用返回同一个实例
const store2 = useUserStore() // 复用
console.log(store === store2) // true

3: 如何在 Pinia 中实现持久化?

答案

方法 1:使用插件

bash
npm install pinia-plugin-persistedstate
typescript
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  return { user }
}, {
  persist: true
})

方法 2:手动实现

typescript
export const useUserStore = defineStore('user', () => {
  const user = ref(null)

  // 从 localStorage 恢复
  const saved = localStorage.getItem('user')
  if (saved) {
    user.value = JSON.parse(saved)
  }

  // 监听变化
  watch(user, (newUser) => {
    localStorage.setItem('user', JSON.stringify(newUser))
  }, { deep: true })

  return { user }
})

4: Pinia 如何实现时间旅行?

答案

通过 $subscribe 记录每次 state 变化:

typescript
const history: any[] = []
let currentIndex = -1

store.$subscribe((mutation, state) => {
  // 记录当前状态
  history.push(JSON.parse(JSON.stringify(state)))
  currentIndex = history.length - 1
})

function undo() {
  if (currentIndex > 0) {
    currentIndex--
    store.$patch(history[currentIndex])
  }
}

function redo() {
  if (currentIndex < history.length - 1) {
    currentIndex++
    store.$patch(history[currentIndex])
  }
}

5: 如何在 setup 外使用 Pinia Store?

答案

typescript
// ❌ 错误 - 在模块顶层调用
import { useUserStore } from '@/stores/user'
const store = useUserStore() // pinia 还未安装

// ✅ 正确 - 在函数中调用
import { useUserStore } from '@/stores/user'

export function someFunction() {
  const store = useUserStore() // pinia 已安装
  return store.user
}

// ✅ 正确 - 传入 pinia 实例
import { useUserStore } from '@/stores/user'
import { pinia } from '@/main'

const store = useUserStore(pinia)

6: Pinia 的 Setup Store 和 Options Store 有什么区别?

答案

Setup Store(推荐):

  • 使用 Composition API
  • 更灵活
  • 更好的 TypeScript 支持
  • 可以使用组合式函数

Options Store

  • 类似 Vuex
  • 更熟悉的语法
  • 适合从 Vuex 迁移
typescript
// Setup Store
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const double = computed(() => count.value * 2)
  function increment() { count.value++ }
  return { count, double, increment }
})

// Options Store
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: { double: (state) => state.count * 2 },
  actions: { increment() { this.count++ } }
})

7: 如何在 Pinia 中处理异步操作?

答案

直接在 actions 中使用 async/await:

typescript
export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)

  async function fetchUser(id: number) {
    loading.value = true
    error.value = null

    try {
      const response = await api.getUser(id)
      user.value = response.data
      return response.data
    } catch (e) {
      error.value = e
      throw e
    } finally {
      loading.value = false
    }
  }

  return { user, loading, error, fetchUser }
})

8: Pinia Store 之间如何通信?

答案

直接在一个 Store 中引用另一个 Store:

typescript
export const useCartStore = defineStore('cart', () => {
  const userStore = useUserStore() // 引用其他 store

  const items = ref([])

  function addItem(item) {
    if (!userStore.isLoggedIn) {
      throw new Error('Please login first')
    }
    items.value.push(item)
  }

  return { items, addItem }
})

9: 如何测试 Pinia Store?

答案

typescript
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from './counter'

describe('Counter Store', () => {
  beforeEach(() => {
    // 创建新的 pinia 实例
    setActivePinia(createPinia())
  })

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

  it('doubles count', () => {
    const store = useCounterStore()
    store.count = 5
    
    expect(store.doubleCount).toBe(10)
  })
})

10: Pinia 的插件系统如何工作?

答案

插件是一个函数,接收 PiniaPluginContext 参数:

typescript
export function myPlugin({ store, app, pinia, options }: PiniaPluginContext) {
  // 为所有 store 添加属性
  store.$customProperty = 'custom'

  // 监听 state 变化
  store.$subscribe((mutation, state) => {
    console.log('State changed')
  })

  // 监听 action
  store.$onAction(({ name, args }) => {
    console.log(`Action ${name} called with`, args)
  })

  // 返回对象会合并到 store
  return {
    $customMethod() {
      console.log('Custom method')
    }
  }
}

// 使用
const pinia = createPinia()
pinia.use(myPlugin)