Store 是 Pinia 的核心概念,它是存储应用程序状态、业务逻辑和计算属性的容器。每个 Store 都是一个独立的模块,可以按功能划分,使得状态管理更加模块化和可维护。
在 Pinia 中,Store 使用 defineStore() 函数定义,每个 Store 都有唯一的 ID,并包含三个主要部分:
| 组成部分 | 说明 | 特点 | 示例 |
|---|---|---|---|
| State | 存储应用程序的状态数据 | 响应式、可序列化、可持久化 | count: 0 |
| Getters | 计算属性,派生状态 | 缓存、组合、参数化 | doubleCount: state => state.count * 2 |
| Actions | 执行业务逻辑,修改状态 | 同步/异步、可组合、支持错误处理 | increment() { this.count++ } |
Pinia 提供了两种方式来创建 Store:Options API 风格和 Composition API 风格。
这是最常用的方式,类似于 Vue 的 Options API,结构清晰易懂。
// 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()
}
}
})
这种方式更灵活,适合复杂的 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
}
})
Pinia 对 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()
}
}
}
})
// 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
<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>
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 模块化设计是构建可维护应用的关键。
// 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 }
]
}
}
})
Pinia 的插件系统允许扩展 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')
有时需要在组件外部(如路由守卫、工具函数、API 模块)访问 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 是构建现代化 Vue 应用状态管理的核心。通过合理的 Store 设计,可以创建出可维护、可测试、高性能的应用程序。
关键要点回顾:
defineStore 创建 Store,支持 Options 和 Composition 两种风格掌握 Pinia Store 的创建和使用技巧,能够帮助你构建出更加健壮和可维护的 Vue 应用程序。