Vue.jsPinia Store

Store 是 Pinia 的核心概念,它是存储应用程序状态、业务逻辑和计算属性的容器。每个 Store 都是一个独立的模块,可以按功能划分,使得状态管理更加模块化和可维护。

Store 基础概念

在 Pinia 中,Store 使用 defineStore() 函数定义,每个 Store 都有唯一的 ID,并包含三个主要部分:

组成部分 说明 特点 示例
State 存储应用程序的状态数据 响应式、可序列化、可持久化 count: 0
Getters 计算属性,派生状态 缓存、组合、参数化 doubleCount: state => state.count * 2
Actions 执行业务逻辑,修改状态 同步/异步、可组合、支持错误处理 increment() { this.count++ }
Store 设计原则
  • 单一职责:每个 Store 只关注一个特定的业务领域
  • 独立自治:Store 之间保持独立,通过组合使用
  • 类型安全:充分利用 TypeScript 的类型系统
  • 可测试性:设计易于测试的 Store 结构
  • 可维护性:保持代码清晰、文档完整

创建 Store

Pinia 提供了两种方式来创建 Store:Options API 风格和 Composition API 风格。

Options API 风格

这是最常用的方式,类似于 Vue 的 Options API,结构清晰易懂。

示例Options API 风格的 Store

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

export const useCounterStore = defineStore('counter', {
  // State: 使用函数返回状态对象
  state: () => ({
    count: 0,
    name: '计数器',
    lastUpdated: null
  }),

  // Getters: 计算属性
  getters: {
    // 基本 getter
    doubleCount: (state) => state.count * 2,

    // 使用其他 getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    },

    // 带参数的 getter
    multiplyBy: (state) => {
      return (multiplier) => state.count * multiplier
    },

    // 复杂计算
    formattedLastUpdated: (state) => {
      if (!state.lastUpdated) return '从未更新'
      return `最后更新: ${new Date(state.lastUpdated).toLocaleString()}`
    }
  },

  // Actions: 执行业务逻辑
  actions: {
    // 同步 action
    increment() {
      this.count++
      this.lastUpdated = new Date().toISOString()
    },

    decrement() {
      this.count--
      this.lastUpdated = new Date().toISOString()
    },

    // 带参数的 action
    incrementBy(amount) {
      this.count += amount
      this.lastUpdated = new Date().toISOString()
    },

    // 异步 action
    async incrementAsync() {
      // 模拟异步操作
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count++
      this.lastUpdated = new Date().toISOString()
    },

    // 重置状态
    reset() {
      // 使用 $reset 方法重置到初始状态
      this.$reset()
    },

    // 批量更新
    updateCounter(newState) {
      // 使用 $patch 批量更新
      this.$patch({
        count: newState.count || this.count,
        name: newState.name || this.name
      })
      this.lastUpdated = new Date().toISOString()
    }
  }
})
                        

Composition API 风格

这种方式更灵活,适合复杂的 Store 逻辑。

示例Composition API 风格的 Store

// stores/todo.js
import { defineStore } from 'pinia'
import { ref, computed, reactive } from 'vue'

export const useTodoStore = defineStore('todo', () => {
  // State - 使用 ref 或 reactive
  const todos = ref([])
  const filter = ref('all')
  const nextId = ref(1)

  // 使用 reactive 创建复杂状态
  const uiState = reactive({
    isLoading: false,
    error: null,
    sortBy: 'date'
  })

  // Getters - 使用 computed
  const completedTodos = computed(() =>
    todos.value.filter(todo => todo.completed)
  )

  const activeTodos = computed(() =>
    todos.value.filter(todo => !todo.completed)
  )

  const filteredTodos = computed(() => {
    switch (filter.value) {
      case 'completed':
        return completedTodos.value
      case 'active':
        return activeTodos.value
      default:
        return todos.value
    }
  })

  const todoCount = computed(() => ({
    total: todos.value.length,
    completed: completedTodos.value.length,
    active: activeTodos.value.length
  }))

  // Actions - 普通函数
  async function fetchTodos() {
    uiState.isLoading = true
    uiState.error = null

    try {
      // 模拟 API 调用
      const response = await fetch('/api/todos')
      const data = await response.json()
      todos.value = data
    } catch (error) {
      uiState.error = error.message
      console.error('获取待办事项失败:', error)
    } finally {
      uiState.isLoading = false
    }
  }

  function addTodo(title) {
    if (!title.trim()) return

    const newTodo = {
      id: nextId.value++,
      title: title.trim(),
      completed: false,
      createdAt: new Date().toISOString()
    }

    todos.value.push(newTodo)
    return newTodo
  }

  function toggleTodo(id) {
    const todo = todos.value.find(t => t.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }

  function removeTodo(id) {
    const index = todos.value.findIndex(t => t.id === id)
    if (index !== -1) {
      todos.value.splice(index, 1)
    }
  }

  function clearCompleted() {
    todos.value = todos.value.filter(todo => !todo.completed)
  }

  function updateFilter(newFilter) {
    filter.value = newFilter
  }

  // 返回所有需要暴露的状态和方法
  return {
    // State
    todos,
    filter,
    uiState,

    // Getters
    completedTodos,
    activeTodos,
    filteredTodos,
    todoCount,

    // Actions
    fetchTodos,
    addTodo,
    toggleTodo,
    removeTodo,
    clearCompleted,
    updateFilter
  }
})
                        
两种风格的选择
  • Options API:适合大多数场景,结构清晰,易于理解
  • Composition API:适合复杂逻辑,更灵活,类型推断更好
  • 推荐:新项目建议使用 Composition API 风格,现有项目可根据团队习惯选择

TypeScript 支持

Pinia 对 TypeScript 有优秀的支持,可以完全类型安全地使用 Store。

定义类型化的 Store

TypeScript完整的类型化 Store 示例

// types/todo.ts
export interface Todo {
  id: number
  title: string
  completed: boolean
  createdAt: string
  updatedAt?: string
}

export interface TodoState {
  todos: Todo[]
  filter: 'all' | 'active' | 'completed'
  loading: boolean
  error: string | null
}

// stores/todo.ts
import { defineStore } from 'pinia'
import type { Todo, TodoState } from '@/types/todo'

export const useTodoStore = defineStore('todo', {
  // State 类型
  state: (): TodoState => ({
    todos: [],
    filter: 'all',
    loading: false,
    error: null
  }),

  // Getters 类型
  getters: {
    completedTodos(state): Todo[] {
      return state.todos.filter(todo => todo.completed)
    },

    activeTodos(state): Todo[] {
      return state.todos.filter(todo => !todo.completed)
    },

    filteredTodos(state): Todo[] {
      switch (state.filter) {
        case 'completed':
          return this.completedTodos
        case 'active':
          return this.activeTodos
        default:
          return state.todos
      }
    },

    // 带参数的 getter
    getTodoById: (state) => {
      return (id: number): Todo | undefined => {
        return state.todos.find(todo => todo.id === id)
      }
    },

    // 计算属性类型
    stats(state) {
      return {
        total: state.todos.length,
        completed: this.completedTodos.length,
        active: this.activeTodos.length
      }
    }
  },

  // Actions 类型
  actions: {
    async fetchTodos(): Promise<void> {
      this.loading = true
      this.error = null

      try {
        const response = await fetch('/api/todos')
        const data: Todo[] = await response.json()
        this.todos = data
      } catch (error) {
        this.error = error instanceof Error ? error.message : '未知错误'
      } finally {
        this.loading = false
      }
    },

    addTodo(title: string): Todo {
      if (!title.trim()) {
        throw new Error('标题不能为空')
      }

      const newTodo: Todo = {
        id: Date.now(),
        title: title.trim(),
        completed: false,
        createdAt: new Date().toISOString()
      }

      this.todos.push(newTodo)
      return newTodo
    },

    updateTodo(id: number, updates: Partial<Todo>): void {
      const todo = this.todos.find(t => t.id === id)
      if (todo) {
        Object.assign(todo, updates, {
          updatedAt: new Date().toISOString()
        })
      }
    },

    deleteTodo(id: number): void {
      const index = this.todos.findIndex(t => t.id === id)
      if (index !== -1) {
        this.todos.splice(index, 1)
      }
    },

    toggleTodo(id: number): void {
      const todo = this.getTodoById(id)
      if (todo) {
        todo.completed = !todo.completed
        todo.updatedAt = new Date().toISOString()
      }
    }
  }
})
                        

全局类型声明

TypeScript扩展 Pinia 类型

// types/pinia.d.ts
import 'pinia'

// 扩展 RootState 类型
declare module 'pinia' {
  export interface PiniaCustomProperties {
    // 自定义属性
    $myPluginProperty: string

    // 自定义方法
    $myPluginMethod(): void
  }

  // 扩展 Store 定义
  export interface DefineStoreOptionsBase<S, Store> {
    // 允许配置自定义选项
    persist?: boolean
  }

  // 扩展 Store 泛型
  export interface StoreProperties<Id extends string> {
    $customProperty: string
  }
}

// 在组件中使用时获得完整的类型提示
const store = useTodoStore()
store.$customProperty // 类型: string
store.$myPluginMethod() // 类型: () => void
                        

Store 使用方式

基本使用

示例在组件中使用 Store

<template>
  <div>
    <!-- Composition API -->
    <div v-if="compositionStore">
      <h3>Composition API 方式</h3>
      <p>计数: {{ compositionStore.count }}</p>
      <p>双倍计数: {{ doubleCount }}</p>
      <button @click="compositionStore.increment()">增加</button>
    </div>

    <!-- Options API -->
    <div v-if="optionsStore">
      <h3>Options API 方式</h3>
      <p>计数: {{ count }}</p>
      <p>双倍计数: {{ doubleCountOption }}</p>
      <button @click="increment">增加</button>
    </div>

    <!-- 直接访问 -->
    <div>
      <h3>直接访问方式</h3>
      <p>Store ID: {{ storeId }}</p>
      <p>完整状态: {{ storeState }}</p>
    </div>
  </div>
</template>

<script setup>
// Composition API 方式
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const compositionStore = useCounterStore()

// 解构保持响应性
const { count } = storeToRefs(compositionStore)

// 直接访问 getter
const doubleCount = computed(() => compositionStore.doubleCount)

// Options API 方式(在 setup 中)
import { mapState, mapActions } from 'pinia'
import { computed } from 'vue'

// 或者使用 map helpers
const optionsStore = useCounterStore()

// 计算属性
const countOption = computed(() => optionsStore.count)
const doubleCountOption = computed(() => optionsStore.doubleCount)

// 方法
const increment = () => optionsStore.increment()

// 直接访问 Store 属性
const storeId = compositionStore.$id
const storeState = compositionStore.$state
</script>

<!-- Options API 组件 -->
<script>
// Options API 方式(传统)
import { mapState, mapActions } from 'pinia'

export default {
  name: 'CounterComponent',
  computed: {
    // 映射 state
    ...mapState(useCounterStore, ['count']),

    // 映射 getter
    ...mapState(useCounterStore, {
      doubleCount: 'doubleCount'
    }),

    // 自定义计算属性
    formattedCount() {
      return `计数: ${this.count}`
    }
  },
  methods: {
    // 映射 actions
    ...mapActions(useCounterStore, ['increment', 'decrement'])
  },
  mounted() {
    // 直接访问 store 实例
    const store = useCounterStore()
    console.log('Store 实例:', store)
  }
}
</script>
                        

Store 操作

示例Store 的各种操作方法

import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()

// 1. 访问状态
console.log('当前计数:', store.count)
console.log('完整状态:', store.$state)
console.log('Store ID:', store.$id)

// 2. 修改状态
// 直接修改
store.count = 10

// 使用 action
store.increment()
store.incrementBy(5)

// 使用 $patch 批量更新
store.$patch({
  count: 20,
  name: '新的计数器名称'
})

// 使用 $patch 函数形式
store.$patch((state) => {
  state.count += 10
  state.lastUpdated = new Date().toISOString()
})

// 3. 订阅状态变化
const unsubscribe = store.$subscribe((mutation, state) => {
  console.log('状态变化:', mutation.type)
  console.log('新状态:', state)
})

// 取消订阅
unsubscribe()

// 4. 订阅 Action
store.$onAction(({ name, store, args, after, onError }) => {
  console.log(`Action ${name} 开始执行,参数:`, args)

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

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

// 5. 重置状态
store.$reset()

// 6. 替换状态
store.$state = { count: 0, name: '重置的计数器' }

// 7. 持久化状态(需要插件)
localStorage.setItem('counter-store', JSON.stringify(store.$state))

// 8. 检查状态变化
console.log('是否已修改:', store.$isChanged)

// 9. 自定义属性访问
console.log('自定义属性:', store.$customProperty)
                        

Store 模块化

合理的 Store 模块化设计是构建可维护应用的关键。

按功能划分 Store

stores/
├── index.js // 导出所有 Store
├── auth.js // 认证相关状态
├── user.js // 用户相关状态
├── product.js // 商品相关状态
├── cart.js // 购物车状态
├── order.js // 订单状态
├── ui.js // UI 状态
└── notification.js // 通知状态

Store 组合使用

示例Store 之间的组合使用

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

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: null,
    isAuthenticated: false
  }),

  actions: {
    login(credentials) {
      // 登录逻辑
      this.user = { id: 1, name: '张三' }
      this.token = 'fake-token'
      this.isAuthenticated = true
    },

    logout() {
      this.$reset()
    }
  }
})

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

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    total: 0
  }),

  getters: {
    itemCount: (state) => state.items.length,
    isEmpty: (state) => state.items.length === 0
  },

  actions: {
    addItem(product) {
      const existingItem = this.items.find(item => item.id === product.id)

      if (existingItem) {
        existingItem.quantity++
      } else {
        this.items.push({
          ...product,
          quantity: 1
        })
      }

      this.calculateTotal()
    },

    removeItem(productId) {
      this.items = this.items.filter(item => item.id !== productId)
      this.calculateTotal()
    },

    calculateTotal() {
      this.total = this.items.reduce((sum, item) =>
        sum + (item.price * item.quantity), 0
      )
    },

    // 使用其他 Store
    async checkout() {
      const authStore = useAuthStore()

      if (!authStore.isAuthenticated) {
        throw new Error('请先登录')
      }

      // 模拟结账过程
      const order = {
        userId: authStore.user.id,
        items: this.items,
        total: this.total,
        createdAt: new Date().toISOString()
      }

      // 清空购物车
      this.items = []
      this.total = 0

      return order
    }
  }
})

// stores/order.js
import { defineStore } from 'pinia'
import { useAuthStore } from './auth'
import { useCartStore } from './cart'

export const useOrderStore = defineStore('order', {
  state: () => ({
    orders: [],
    currentOrder: null
  }),

  actions: {
    async createOrder() {
      const authStore = useAuthStore()
      const cartStore = useCartStore()

      // 验证
      if (!authStore.isAuthenticated) {
        throw new Error('请先登录')
      }

      if (cartStore.isEmpty) {
        throw new Error('购物车为空')
      }

      // 创建订单
      const order = await cartStore.checkout()
      this.orders.push(order)
      this.currentOrder = order

      return order
    },

    async fetchOrders() {
      const authStore = useAuthStore()

      if (!authStore.isAuthenticated) {
        throw new Error('请先登录')
      }

      // 模拟获取用户订单
      this.orders = [
        { id: 1, userId: authStore.user.id, total: 100 }
      ]
    }
  }
})
                        

Store 插件

Pinia 的插件系统允许扩展 Store 的功能。

创建自定义插件

示例创建和使用 Store 插件

// plugins/pinia-plugin.js

// 1. 持久化插件
export const piniaPersistPlugin = ({ store }) => {
  // 从 localStorage 恢复状态
  const storageKey = `pinia-${store.$id}`
  const savedState = localStorage.getItem(storageKey)

  if (savedState) {
    store.$patch(JSON.parse(savedState))
  }

  // 监听状态变化
  store.$subscribe((mutation, state) => {
    localStorage.setItem(storageKey, JSON.stringify(state))
  })
}

// 2. 日志插件
export const piniaLoggerPlugin = ({ store }) => {
  // 监听所有变化
  store.$subscribe((mutation, state) => {
    console.group(`📦 Store: ${store.$id}`)
    console.log('Mutation:', mutation)
    console.log('State:', state)
    console.groupEnd()
  })

  // 监听所有 actions
  store.$onAction(({ name, store, args, after, onError }) => {
    const startTime = Date.now()

    console.group(`⚡ Action: ${name}`)
    console.log('Args:', args)

    after((result) => {
      const duration = Date.now() - startTime
      console.log(`✅ 完成 (${duration}ms)`)
      console.log('Result:', result)
      console.groupEnd()
    })

    onError((error) => {
      const duration = Date.now() - startTime
      console.error(`❌ 失败 (${duration}ms):`, error)
      console.groupEnd()
    })
  })
}

// 3. 重置确认插件
export const piniaResetConfirmPlugin = ({ store }) => {
  const originalReset = store.$reset

  store.$reset = function() {
    const confirmed = confirm(`确定要重置 ${store.$id} 的状态吗?`)
    if (confirmed) {
      originalReset.call(this)
    }
  }
}

// 4. 自定义属性插件
export const piniaCustomPropertiesPlugin = ({ store }) => {
  // 添加自定义属性
  store.$customProperty = '这是一个自定义属性'

  // 添加自定义方法
  store.$customMethod = function() {
    console.log(`自定义方法被调用: ${store.$id}`)
  }

  // 添加工具方法
  store.$toJSON = function() {
    return JSON.stringify(this.$state, null, 2)
  }
}

// 5. 组合插件
export const piniaAllPlugins = [
  piniaPersistPlugin,
  piniaLoggerPlugin,
  piniaResetConfirmPlugin,
  piniaCustomPropertiesPlugin
]

// main.js 中使用
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { piniaAllPlugins } from './plugins/pinia-plugin'

const pinia = createPinia()

// 注册所有插件
piniaAllPlugins.forEach(plugin => pinia.use(plugin))

const app = createApp(App)
app.use(pinia)
app.mount('#app')
                        

Store 在组件外的使用

有时需要在组件外部(如路由守卫、工具函数、API 模块)访问 Store。

示例在组件外使用 Store

// utils/api.js - API 模块中使用 Store
import { useAuthStore } from '@/stores/auth'

export class ApiClient {
  constructor() {
    this.authStore = null
  }

  // 延迟获取 Store,确保 Pinia 已安装
  getAuthStore() {
    if (!this.authStore) {
      this.authStore = useAuthStore()
    }
    return this.authStore
  }

  async request(url, options = {}) {
    const authStore = this.getAuthStore()

    // 添加认证头
    const headers = {
      ...options.headers,
      'Content-Type': 'application/json'
    }

    if (authStore.token) {
      headers['Authorization'] = `Bearer ${authStore.token}`
    }

    const config = {
      ...options,
      headers
    }

    try {
      const response = await fetch(url, config)

      if (!response.ok) {
        // 处理认证错误
        if (response.status === 401) {
          authStore.logout()
          throw new Error('认证已过期,请重新登录')
        }
        throw new Error(`HTTP ${response.status}`)
      }

      return await response.json()
    } catch (error) {
      console.error('API 请求失败:', error)
      throw error
    }
  }
}

// router/index.js - 路由守卫中使用 Store
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // 路由配置
  ]
})

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 在守卫中获取 Store
  const authStore = useAuthStore()

  // 检查路由是否需要认证
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next({ name: 'login', query: { redirect: to.fullPath } })
    return
  }

  // 检查是否只允许未登录用户访问
  if (to.meta.guestOnly && authStore.isAuthenticated) {
    next({ name: 'home' })
    return
  }

  next()
})

// 工具函数中使用 Store
import { useNotificationStore } from '@/stores/notification'

export function showSuccess(message) {
  const notificationStore = useNotificationStore()
  notificationStore.addNotification({
    type: 'success',
    message,
    duration: 3000
  })
}

export function showError(message) {
  const notificationStore = useNotificationStore()
  notificationStore.addNotification({
    type: 'error',
    message,
    duration: 5000
  })
}

// 在其他模块中调用
showSuccess('操作成功!')
showError('操作失败,请重试!')
                        

Store 功能演示

Pinia Store 功能演示

模拟一个完整的用户和购物车 Store 交互:

用户管理:
未登录
用户信息将在这里显示
购物车操作:
购物车: 0 件商品
总价: ¥0.00

Store 操作:
查看控制台查看详细操作日志
当前 Store 状态:

等待操作...

点击上方按钮模拟 Store 的各种操作,查看控制台输出

Store 最佳实践

Store 设计最佳实践
  • 合理的模块划分:按业务功能划分 Store,避免单个 Store 过于庞大
  • 清晰的命名规范:使用一致的命名规则,如 useXxxStore
  • 完整的类型定义:充分利用 TypeScript,提供完整的类型支持
  • 单一职责原则:每个 Store 只负责一个特定的业务领域
  • 错误处理机制:在 Action 中妥善处理错误,提供友好的用户反馈
  • 可测试性设计:设计易于测试的 Store 结构,便于编写单元测试
  • 性能优化考虑:避免在 Getter 中执行昂贵计算,合理使用缓存
  • 文档和示例:为复杂的 Store 提供文档和使用示例
常见陷阱
  • 循环依赖:避免 Store 之间的循环引用
  • 过度解构:注意解构 Store 时可能丢失响应性
  • 状态污染:避免直接修改嵌套对象的属性,使用 $patch
  • 内存泄漏:及时清理订阅和监听器
  • 类型安全:注意 TypeScript 类型推断的局限性

总结

Pinia Store 是构建现代化 Vue 应用状态管理的核心。通过合理的 Store 设计,可以创建出可维护、可测试、高性能的应用程序。

关键要点回顾:

  • Store 定义:使用 defineStore 创建 Store,支持 Options 和 Composition 两种风格
  • 状态管理:State 存储数据,Getter 计算派生状态,Action 执行业务逻辑
  • 类型安全:完整的 TypeScript 支持,提供优秀的开发体验
  • 模块化设计:按功能划分 Store,保持代码清晰和可维护
  • 插件系统:可扩展的插件系统,支持自定义功能
  • 响应式操作:支持订阅、批量更新、重置等高级操作
  • 组件外使用:可以在路由守卫、工具函数等非组件环境中使用 Store

掌握 Pinia Store 的创建和使用技巧,能够帮助你构建出更加健壮和可维护的 Vue 应用程序。

测试你的理解

思考题
  1. Options API 和 Composition API 两种 Store 定义方式各有什么优缺点?
  2. 如何在 TypeScript 中为 Store 提供完整的类型支持?
  3. Store 之间如何安全地相互调用?需要注意什么?
  4. 如何在非组件环境中(如路由守卫)正确使用 Store?
  5. 设计一个可维护的 Store 模块化结构需要考虑哪些因素?