Setup Store 组合式风格 #
什么是 Setup Store? #
Setup Store 是 Pinia 提供的另一种定义 Store 的方式,它使用函数语法,类似于 Vue 3 的 Composition API 中的 setup() 函数。
对比 Options Store #
ts
// Options Store
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2
},
actions: {
increment() { this.count++ }
}
})
// 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 }
})
基本语法 #
State:使用 ref 或 reactive #
ts
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
export const useUserStore = defineStore('user', () => {
// 使用 ref 定义简单状态
const name = ref('John')
const age = ref(25)
// 使用 reactive 定义对象状态
const profile = reactive({
email: 'john@example.com',
address: {
city: 'Beijing',
country: 'China'
}
})
return { name, age, profile }
})
Getters:使用 computed #
ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
// 使用 computed 定义 getter
const doubleCount = computed(() => count.value * 2)
const tripleCount = computed(() => count.value * 3)
// 依赖其他 getter
const quadrupleCount = computed(() => doubleCount.value * 2)
return { count, doubleCount, tripleCount, quadrupleCount }
})
Actions:普通函数 #
ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
// Actions 就是普通函数
function increment() {
count.value++
}
function incrementBy(amount: number) {
count.value += amount
}
function reset() {
count.value = 0
}
// 异步 action
async function fetchInitialValue() {
const response = await fetch('/api/initial-value')
count.value = await response.json()
}
return { count, increment, incrementBy, reset, fetchInitialValue }
})
完整示例 #
用户管理 Store #
ts
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
}
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null)
const token = ref<string | null>(localStorage.getItem('token'))
const loading = ref(false)
const error = ref<string | null>(null)
// Getters
const isAuthenticated = computed(() => !!token.value && !!user.value)
const isAdmin = computed(() => user.value?.role === 'admin')
const displayName = computed(() => user.value?.name || 'Guest')
// Actions
async function login(email: string, password: string) {
loading.value = true
error.value = null
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
const data = await response.json()
user.value = data.user
token.value = data.token
localStorage.setItem('token', data.token)
return true
} catch (e) {
error.value = e instanceof Error ? e.message : 'Login failed'
return false
} finally {
loading.value = false
}
}
async function fetchUser() {
if (!token.value) return
loading.value = true
try {
const response = await fetch('/api/user', {
headers: { Authorization: `Bearer ${token.value}` }
})
user.value = await response.json()
} catch (e) {
logout()
} finally {
loading.value = false
}
}
function logout() {
user.value = null
token.value = null
localStorage.removeItem('token')
}
function clearError() {
error.value = null
}
return {
// State
user,
token,
loading,
error,
// Getters
isAuthenticated,
isAdmin,
displayName,
// Actions
login,
fetchUser,
logout,
clearError
}
})
购物车 Store #
ts
// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface CartItem {
id: number
productId: number
name: string
price: number
quantity: number
}
export const useCartStore = defineStore('cart', () => {
// State
const items = ref<CartItem[]>([])
const discount = ref(0)
// Getters
const itemCount = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
const subtotal = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const discountAmount = computed(() =>
subtotal.value * (discount.value / 100)
)
const total = computed(() =>
subtotal.value - discountAmount.value
)
const isEmpty = computed(() => items.value.length === 0)
// Actions
function addItem(product: Omit<CartItem, 'id' | 'quantity'>) {
const existingItem = items.value.find(item => item.productId === product.productId)
if (existingItem) {
existingItem.quantity++
} else {
items.value.push({
id: Date.now(),
...product,
quantity: 1
})
}
}
function removeItem(itemId: number) {
const index = items.value.findIndex(item => item.id === itemId)
if (index !== -1) {
items.value.splice(index, 1)
}
}
function updateQuantity(itemId: number, quantity: number) {
const item = items.value.find(item => item.id === itemId)
if (item) {
if (quantity <= 0) {
removeItem(itemId)
} else {
item.quantity = quantity
}
}
}
function setDiscount(value: number) {
discount.value = Math.min(100, Math.max(0, value))
}
function clearCart() {
items.value = []
discount.value = 0
}
return {
// State
items,
discount,
// Getters
itemCount,
subtotal,
discountAmount,
total,
isEmpty,
// Actions
addItem,
removeItem,
updateQuantity,
setDiscount,
clearCart
}
})
私有属性和方法 #
Setup Store 可以定义不暴露的私有属性:
ts
export const useCounterStore = defineStore('counter', () => {
// 公共状态
const count = ref(0)
// 私有变量(不返回)
const maxCount = 100
const minCount = 0
// 私有方法(不返回)
function validate(value: number): boolean {
return value >= minCount && value <= maxCount
}
function clamp(value: number): number {
return Math.min(maxCount, Math.max(minCount, value))
}
// 公共方法
function increment() {
count.value = clamp(count.value + 1)
}
function decrement() {
count.value = clamp(count.value - 1)
}
function setCount(value: number) {
if (validate(value)) {
count.value = value
}
}
// 只返回公共属性
return { count, increment, decrement, setCount }
})
使用组合式函数 #
Setup Store 可以使用自定义组合式函数:
定义组合式函数 #
ts
// composables/useAsync.ts
import { ref, Ref } from 'vue'
export function useAsync<T>(asyncFn: () => Promise<T>) {
const data = ref<T | null>(null) as Ref<T | 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 instanceof Error ? e : new Error(String(e))
} finally {
loading.value = false
}
}
return { data, loading, error, execute }
}
在 Store 中使用 #
ts
// stores/product.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useAsync } from '@/composables/useAsync'
interface Product {
id: number
name: string
price: number
}
export const useProductStore = defineStore('product', () => {
const products = ref<Product[]>([])
// 使用组合式函数
const { loading, error, execute: fetchProducts } = useAsync(async () => {
const response = await fetch('/api/products')
products.value = await response.json()
return products.value
})
const { loading: creating, execute: createProduct } = useAsync(async (product: Omit<Product, 'id'>) => {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(product)
})
const newProduct = await response.json()
products.value.push(newProduct)
return newProduct
})
const productCount = computed(() => products.value.length)
return {
products,
loading,
error,
creating,
productCount,
fetchProducts,
createProduct
}
})
重置状态 #
Setup Store 需要手动实现 $reset:
ts
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('counter')
// 初始状态
const initialState = {
count: 0,
name: 'counter'
}
function $reset() {
count.value = initialState.count
name.value = initialState.name
}
function increment() {
count.value++
}
return { count, name, increment, $reset }
})
使用工厂函数 #
ts
export const useUserStore = defineStore('user', () => {
// 工厂函数
function createInitialState() {
return {
name: '',
email: '',
age: 0
}
}
const state = reactive(createInitialState())
function $reset() {
Object.assign(state, createInitialState())
}
return { ...toRefs(state), $reset }
})
访问其他 Store #
ts
// stores/order.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useCartStore } from './cart'
import { useUserStore } from './user'
export const useOrderStore = defineStore('order', () => {
const orders = ref<Order[]>([])
async function createOrder() {
const cartStore = useCartStore()
const userStore = useUserStore()
const order = {
userId: userStore.user?.id,
items: [...cartStore.items],
total: cartStore.total,
createdAt: new Date()
}
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(order)
})
const newOrder = await response.json()
orders.value.push(newOrder)
cartStore.clearCart()
return newOrder
}
return { orders, createOrder }
})
Setup Store vs Options Store #
| 特性 | Setup Store | Options Store |
|---|---|---|
| 语法 | 函数式 | 对象式 |
| 灵活性 | 高 | 固定结构 |
| 私有属性 | 支持 | 不支持 |
| 组合式函数 | 可使用 | 不可使用 |
| TypeScript | 自动推断 | 需要注解 |
| $reset | 需手动实现 | 自动支持 |
| 学习曲线 | 需了解 Composition API | 较低 |
何时使用 Setup Store #
- 需要私有属性或方法
- 需要使用组合式函数
- 复杂的状态逻辑
- 需要更好的 TypeScript 推断
- 熟悉 Composition API
何时使用 Options Store #
- 刚接触 Pinia
- 简单的状态管理
- 喜欢固定结构
- 团队约定
下一步 #
现在你已经掌握了 Setup Store 的使用,接下来让我们学习组合式函数的更多应用。
- 组合式函数 - 与组合式函数配合
最后更新:2026-03-28