Appearance
Pinia Getters
Getters是Pinia Store中的计算属性,用于从State派生数据。它们类似于Vue组件中的computed属性,会自动追踪依赖并在依赖变化时重新计算。本章将详细介绍Getters的使用方法、高级特性和最佳实践。
Getters基础
定义和使用Getters
typescript
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
items: [] as Item[],
user: {
name: 'John',
age: 30
}
}),
getters: {
// 基础getter - 无参数
doubleCount: (state) => state.count * 2,
// 带逻辑的getter
isEven: (state) => state.count % 2 === 0,
// 复杂计算的getter
summary: (state) => {
return `Count is ${state.count}, items: ${state.items.length}, user: ${state.user.name}`
},
// 访问其他getter
doubleCountPlusOne: (state) => {
// 注意:在getter中使用this访问其他getter
return this.doubleCount + 1
}
}
})
interface Item {
id: string
name: string
price: number
}
在组件中使用Getters
vue
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<p>Is Even: {{ counterStore.isEven }}</p>
<p>Summary: {{ counterStore.summary }}</p>
<p>Double Count + 1: {{ counterStore.doubleCountPlusOne }}</p>
<button @click="counterStore.count++">Increment</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
带参数的Getters
返回函数的Getters
typescript
export const useUserStore = defineStore('user', {
state: () => ({
users: [
{ id: '1', name: 'Alice', role: 'admin', department: 'IT' },
{ id: '2', name: 'Bob', role: 'user', department: 'HR' },
{ id: '3', name: 'Charlie', role: 'user', department: 'IT' },
{ id: '4', name: 'Diana', role: 'admin', department: 'Finance' }
] as User[]
}),
getters: {
// 根据ID获取用户
getUserById: (state) => {
return (id: string) => {
return state.users.find(user => user.id === id)
}
},
// 根据角色过滤用户
getUsersByRole: (state) => {
return (role: string) => {
return state.users.filter(user => user.role === role)
}
},
// 根据部门过滤用户
getUsersByDepartment: (state) => {
return (department: string) => {
return state.users.filter(user => user.department === department)
}
},
// 搜索用户
searchUsers: (state) => {
return (query: string) => {
return state.users.filter(user =>
user.name.toLowerCase().includes(query.toLowerCase())
)
}
},
// 获取用户统计信息
getUserStats: (state) => {
return () => {
const total = state.users.length
const admins = state.users.filter(u => u.role === 'admin').length
const itDept = state.users.filter(u => u.department === 'IT').length
return {
total,
admins,
adminPercentage: total > 0 ? (admins / total) * 100 : 0,
itDept
}
}
}
}
})
interface User {
id: string
name: string
role: string
department: string
}
在组件中使用带参数的Getters
vue
<template>
<div>
<h2>User Management</h2>
<!-- 搜索功能 -->
<div>
<input
v-model="searchQuery"
placeholder="Search users..."
@input="performSearch"
/>
</div>
<!-- 显示搜索结果 -->
<div v-if="searchResults.length > 0">
<h3>Search Results:</h3>
<ul>
<li v-for="user in searchResults" :key="user.id">
{{ user.name }} ({{ user.role }} - {{ user.department }})
</li>
</ul>
</div>
<!-- 部门过滤 -->
<div>
<h3>Filter by Department:</h3>
<button
v-for="dept in departments"
:key="dept"
@click="filterByDepartment(dept)"
:class="{ active: currentDepartment === dept }"
>
{{ dept }} ({{ getUsersByDepartment(dept).length }})
</button>
</div>
<!-- 显示过滤结果 -->
<div>
<h3>Users in {{ currentDepartment }}:</h3>
<ul>
<li v-for="user in filteredUsers" :key="user.id">
{{ user.name }} ({{ user.role }})
</li>
</ul>
</div>
<!-- 统计信息 -->
<div>
<h3>Statistics:</h3>
<p>Total Users: {{ userStats.total }}</p>
<p>Admins: {{ userStats.admins }} ({{ userStats.adminPercentage.toFixed(1) }}%)</p>
<p>IT Department: {{ userStats.itDept }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { getUsersByDepartment, searchUsers, getUserStats } = userStore
const searchQuery = ref('')
const currentDepartment = ref('All')
// 使用storeToRefs保持响应性
const { users } = storeToRefs(userStore)
const departments = computed(() => {
return [...new Set(users.value.map(user => user.department))]
})
const searchResults = computed(() => {
if (!searchQuery.value) return []
return searchUsers(searchQuery.value)
})
const filteredUsers = computed(() => {
if (currentDepartment.value === 'All') {
return users.value
}
return getUsersByDepartment(currentDepartment.value)
})
const userStats = computed(() => {
return getUserStats()
})
const performSearch = () => {
// 搜索逻辑在computed中处理
}
const filterByDepartment = (dept) => {
currentDepartment.value = dept
}
</script>
<style scoped>
.active {
font-weight: bold;
background-color: #007bff;
color: white;
}
</style>
Getter中的复杂逻辑
计算和聚合
typescript
export const useAnalyticsStore = defineStore('analytics', {
state: () => ({
events: [
{ id: '1', type: 'click', value: 10, timestamp: Date.now() - 86400000 },
{ id: '2', type: 'view', value: 5, timestamp: Date.now() - 43200000 },
{ id: '3', type: 'click', value: 15, timestamp: Date.now() - 3600000 },
{ id: '4', type: 'purchase', value: 100, timestamp: Date.now() - 1800000 }
] as AnalyticsEvent[],
userInteractions: [
{ userId: '1', action: 'login', timestamp: Date.now() - 7200000 },
{ userId: '1', action: 'click', timestamp: Date.now() - 3600000 },
{ userId: '2', action: 'login', timestamp: Date.now() - 1800000 }
] as UserInteraction[]
}),
getters: {
// 基础统计
totalEvents: (state) => state.events.length,
totalValue: (state) => state.events.reduce((sum, event) => sum + event.value, 0),
// 按类型分组
eventsByType: (state) => {
const grouped: Record<string, AnalyticsEvent[]> = {}
state.events.forEach(event => {
if (!grouped[event.type]) {
grouped[event.type] = []
}
grouped[event.type].push(event)
})
return grouped
},
// 类型统计
eventCountsByType: (state) => {
const counts: Record<string, number> = {}
state.events.forEach(event => {
counts[event.type] = (counts[event.type] || 0) + 1
})
return counts
},
// 平均值计算
averageValueByType: (state) => {
const sums: Record<string, number> = {}
const counts: Record<string, number> = {}
state.events.forEach(event => {
sums[event.type] = (sums[event.type] || 0) + event.value
counts[event.type] = (counts[event.type] || 0) + 1
})
const averages: Record<string, number> = {}
Object.keys(sums).forEach(type => {
averages[type] = sums[type] / counts[type]
})
return averages
},
// 时间范围过滤
getEventsInTimeRange: (state) => {
return (startTime: number, endTime: number) => {
return state.events.filter(event =>
event.timestamp >= startTime && event.timestamp <= endTime
)
}
},
// 最近事件
recentEvents: (state) => {
const oneHourAgo = Date.now() - 3600000
return state.events.filter(event => event.timestamp >= oneHourAgo)
},
// 用户活跃度
getUserActivity: (state) => {
const activity: Record<string, number> = {}
state.userInteractions.forEach(interaction => {
activity[interaction.userId] = (activity[interaction.userId] || 0) + 1
})
return activity
},
// 转换率计算
conversionRate: (state) => {
const totalClicks = state.events.filter(e => e.type === 'click').length
const totalPurchases = state.events.filter(e => e.type === 'purchase').length
return totalClicks > 0 ? (totalPurchases / totalClicks) * 100 : 0
}
}
})
interface AnalyticsEvent {
id: string
type: string
value: number
timestamp: number
}
interface UserInteraction {
userId: string
action: string
timestamp: number
}
高级过滤和排序
typescript
export const useProductStore = defineStore('product', {
state: () => ({
products: [
{ id: '1', name: 'Laptop', price: 1200, category: 'Electronics', rating: 4.5, inStock: true },
{ id: '2', name: 'Phone', price: 800, category: 'Electronics', rating: 4.2, inStock: true },
{ id: '3', name: 'Book', price: 20, category: 'Education', rating: 4.8, inStock: false },
{ id: '4', name: 'Headphones', price: 150, category: 'Electronics', rating: 4.0, inStock: true },
{ id: '5', name: 'Desk', price: 300, category: 'Furniture', rating: 4.6, inStock: true }
] as Product[],
filters: {
category: null as string | null,
minPrice: 0,
maxPrice: Infinity,
inStockOnly: false,
minRating: 0
},
sortBy: 'name' as 'name' | 'price' | 'rating',
sortOrder: 'asc' as 'asc' | 'desc'
}),
getters: {
// 基础过滤产品
filteredProducts: (state) => {
return state.products.filter(product => {
// 类别过滤
if (state.filters.category && product.category !== state.filters.category) {
return false
}
// 价格过滤
if (product.price < state.filters.minPrice || product.price > state.filters.maxPrice) {
return false
}
// 库存过滤
if (state.filters.inStockOnly && !product.inStock) {
return false
}
// 评分过滤
if (product.rating < state.filters.minRating) {
return false
}
return true
})
},
// 排序后的产品
sortedProducts: (state) => {
const products = [...this.filteredProducts] // 使用this访问其他getter
return products.sort((a, b) => {
let comparison = 0
switch (state.sortBy) {
case 'name':
comparison = a.name.localeCompare(b.name)
break
case 'price':
comparison = a.price - b.price
break
case 'rating':
comparison = a.rating - b.rating
break
}
return state.sortOrder === 'asc' ? comparison : -comparison
})
},
// 分类统计
categoryStats: (state) => {
const stats: CategoryStats = {}
state.products.forEach(product => {
if (!stats[product.category]) {
stats[product.category] = {
count: 0,
avgPrice: 0,
totalValue: 0,
products: []
}
}
const categoryStat = stats[product.category]
categoryStat.count++
categoryStat.totalValue += product.price
categoryStat.products.push(product)
})
// 计算平均价格
Object.values(stats).forEach(stat => {
stat.avgPrice = stat.count > 0 ? stat.totalValue / stat.count : 0
})
return stats
},
// 价格范围
priceRange: (state) => {
if (state.products.length === 0) {
return { min: 0, max: 0 }
}
const prices = state.products.map(p => p.price)
return {
min: Math.min(...prices),
max: Math.max(...prices)
}
},
// 推荐产品(高评分且有库存)
recommendedProducts: (state) => {
return state.products
.filter(product => product.rating >= 4.5 && product.inStock)
.sort((a, b) => b.rating - a.rating)
.slice(0, 3) // 只取前3个
},
// 产品总数统计
productStats: (state) => {
const total = state.products.length
const inStock = state.products.filter(p => p.inStock).length
const outOfStock = total - inStock
return {
total,
inStock,
outOfStock,
inStockPercentage: total > 0 ? (inStock / total) * 100 : 0
}
}
},
actions: {
setCategoryFilter(category: string | null) {
this.filters.category = category
},
setPriceRange(min: number, max: number) {
this.filters.minPrice = min
this.filters.maxPrice = max
},
setInStockOnly(inStockOnly: boolean) {
this.filters.inStockOnly = inStockOnly
},
setMinRating(minRating: number) {
this.filters.minRating = minRating
},
setSort(sortBy: 'name' | 'price' | 'rating', sortOrder: 'asc' | 'desc = 'asc') {
this.sortBy = sortBy
this.sortOrder = sortOrder
}
}
})
interface Product {
id: string
name: string
price: number
category: string
rating: number
inStock: boolean
}
interface CategoryStats {
[category: string]: {
count: number
avgPrice: number
totalValue: number
products: Product[]
}
}
Getter性能优化
使用computed缓存
typescript
import { computed } from 'vue'
export const useOptimizedStore = defineStore('optimized', {
state: () => ({
largeDataset: Array.from({ length: 10000 }, (_, i) => ({
id: i.toString(),
value: Math.random() * 100,
category: ['A', 'B', 'C'][i % 3],
active: i % 2 === 0
})) as DataItem[],
filters: {
category: null as string | null,
minValue: 0,
maxValue: 100
}
}),
getters: {
// 使用computed包装复杂计算以获得缓存
expensiveCalculatedData: (state) => {
return computed(() => {
// 这个计算会被缓存,只有当依赖变化时才重新计算
return state.largeDataset
.filter(item => {
if (state.filters.category && item.category !== state.filters.category) {
return false
}
if (item.value < state.filters.minValue || item.value > state.filters.maxValue) {
return false
}
return true
})
.map(item => ({
...item,
processedValue: item.value * 2 + 10
}))
.sort((a, b) => b.processedValue - a.processedValue)
})
},
// 带参数的缓存getter
getCachedFilteredData: (state) => {
return (minValue: number, maxValue: number, category: string | null) => {
return computed(() => {
return state.largeDataset.filter(item => {
if (category && item.category !== category) return false
if (item.value < minValue || item.value > maxValue) return false
return true
})
})
}
},
// 分页数据
getPaginatedData: (state) => {
return (page: number, pageSize: number) => {
const start = (page - 1) * pageSize
return state.largeDataset.slice(start, start + pageSize)
}
},
// 统计信息(缓存)
dataStatistics: (state) => {
return computed(() => {
const total = state.largeDataset.length
const activeCount = state.largeDataset.filter(item => item.active).length
const avgValue = state.largeDataset.reduce((sum, item) => sum + item.value, 0) / total
const categories = [...new Set(state.largeDataset.map(item => item.category))]
return {
total,
activeCount,
avgValue,
categories,
activePercentage: total > 0 ? (activeCount / total) * 100 : 0
}
})
}
}
})
interface DataItem {
id: string
value: number
category: string
active: boolean
}
避免Getter中的副作用
typescript
export const usePureGettersStore = defineStore('pureGetters', {
state: () => ({
items: [] as Item[],
lastAccessed: new Map<string, number>()
}),
getters: {
// ✅ 好的实践:纯函数getter
activeItems: (state) => {
return state.items.filter(item => item.active)
},
// ✅ 好的实践:带参数的纯函数getter
getItemsByCategory: (state) => {
return (category: string) => {
return state.items.filter(item => item.category === category)
}
},
// ❌ 避免:有副作用的getter
// updateLastAccessed: (state) => {
// // 这是错误的,getter不应该修改状态
// state.lastAccessed.set('someKey', Date.now())
// return state.items
// },
// ✅ 正确方式:通过action更新状态
itemsWithAccessTime: (state) => {
return state.items.map(item => ({
...item,
lastAccessed: state.lastAccessed.get(item.id) || null
}))
}
},
actions: {
// 在action中更新访问时间
recordAccess(itemId: string) {
this.lastAccessed.set(itemId, Date.now())
},
addItem(item: Item) {
this.items.push(item)
this.recordAccess(item.id)
}
}
})
interface Item {
id: string
name: string
category: string
active: boolean
}
Getter与组件的深度集成
在组件中使用多个Store的Getters
vue
<template>
<div class="dashboard">
<div class="stats">
<h3>System Statistics</h3>
<p>Total Users: {{ userStore.users.length }}</p>
<p>Active Sessions: {{ sessionStore.activeSessionsCount }}</p>
<p>Pending Tasks: {{ taskStore.pendingTasksCount }}</p>
<p>System Load: {{ systemStore.normalizedLoad }}</p>
</div>
<div class="user-list">
<h3>Active Users</h3>
<ul>
<li
v-for="user in filteredActiveUsers"
:key="user.id"
:class="{ premium: user.isPremium }"
>
<span>{{ user.name }}</span>
<span class="role">{{ user.role }}</span>
<span class="status">{{ user.status }}</span>
</li>
</ul>
</div>
<div class="performance">
<h3>Performance Metrics</h3>
<div class="metric" v-for="metric in performanceMetrics" :key="metric.name">
<span class="name">{{ metric.name }}</span>
<span class="value">{{ metric.value }}</span>
<div class="trend" :class="metric.trend">
{{ metric.trend }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
import { useSessionStore } from '@/stores/session'
import { useTaskStore } from '@/stores/task'
import { useSystemStore } from '@/stores/system'
const userStore = useUserStore()
const sessionStore = useSessionStore()
const taskStore = useTaskStore()
const systemStore = useSystemStore()
// 组合多个store的getters
const filteredActiveUsers = computed(() => {
return userStore.users
.filter(user => user.status === 'active')
.sort((a, b) => b.lastLogin - a.lastLogin)
.slice(0, 10) // 只显示最近活跃的10个用户
})
// 计算性能指标
const performanceMetrics = computed(() => [
{
name: 'Response Time',
value: systemStore.avgResponseTime,
trend: systemStore.responseTimeTrend
},
{
name: 'Error Rate',
value: systemStore.errorRate,
trend: systemStore.errorRateTrend
},
{
name: 'Throughput',
value: systemStore.requestsPerSecond,
trend: systemStore.throughputTrend
}
])
</script>
<style scoped>
.dashboard {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
padding: 20px;
}
.stats {
grid-column: 1 / -1;
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
}
.user-list ul {
list-style: none;
padding: 0;
}
.user-list li {
display: flex;
justify-content: space-between;
padding: 8px;
border-bottom: 1px solid #eee;
}
.user-list li.premium {
background-color: #fffde7;
}
.metric {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 0;
}
.trend {
font-weight: bold;
}
.trend.up {
color: green;
}
.trend.down {
color: red;
}
</style>
Getter中的错误处理
typescript
export const useSafeGettersStore = defineStore('safeGetters', {
state: () => ({
data: null as Data[] | null,
loading: false,
error: null as string | null
}),
getters: {
// 安全的数据访问
safeData: (state) => {
if (state.error) {
console.warn('Error in safeData getter:', state.error)
return []
}
return state.data || []
},
// 带默认值的数据访问
processedData: (state) => {
try {
if (!state.data) return []
return state.data.map(item => ({
...item,
processed: true,
timestamp: Date.now()
}))
} catch (error) {
console.error('Error processing data:', error)
return []
}
},
// 统计信息(安全)
dataStats: (state) => {
try {
const data = state.data || []
return {
count: data.length,
avgValue: data.length > 0
? data.reduce((sum, item) => sum + (item.value || 0), 0) / data.length
: 0,
hasData: data.length > 0
}
} catch (error) {
console.error('Error calculating stats:', error)
return {
count: 0,
avgValue: 0,
hasData: false
}
}
},
// 验证数据完整性
isValidData: (state) => {
try {
if (!state.data) return false
// 验证数据结构
return state.data.every(item =>
typeof item.id === 'string' &&
typeof item.name === 'string'
)
} catch (error) {
console.error('Error validating data:', error)
return false
}
}
},
actions: {
async loadData() {
this.loading = true
this.error = null
try {
const response = await fetch('/api/data')
if (!response.ok) throw new Error('Failed to load data')
this.data = await response.json()
} catch (error) {
this.error = (error as Error).message
console.error('Load data error:', error)
} finally {
this.loading = false
}
}
}
})
interface Data {
id: string
name: string
value?: number
}
Getter最佳实践
1. Getter命名约定
typescript
export const useNamingStore = defineStore('naming', {
state: () => ({
users: [] as User[],
settings: {
theme: 'light',
notifications: true
}
}),
getters: {
// ✅ 好的命名:描述性、以动词或形容词开头
activeUsers: (state) => state.users.filter(user => user.active),
userCount: (state) => state.users.length,
hasUsers: (state) => state.users.length > 0,
adminUsers: (state) => state.users.filter(user => user.role === 'admin'),
// ✅ 带参数的getter:使用动词形式
getUsersByRole: (state) => {
return (role: string) => state.users.filter(user => user.role === role)
},
// ❌ 避免:不清晰的命名
// u: (state) => state.users, // 不好
// calc: (state) => ..., // 不好
}
})
2. Getter组合
typescript
export const useComposedGettersStore = defineStore('composedGetters', {
state: () => ({
products: [] as Product[],
cart: [] as CartItem[],
user: null as User | null
}),
getters: {
// 基础getter
productMap: (state) => {
return new Map(state.products.map(p => [p.id, p]))
},
// 组合getter
cartTotal: (state) => {
return state.cart.reduce((total, item) => {
const product = this.productMap.get(item.productId)
return total + (product ? product.price * item.quantity : 0)
}, 0)
},
// 复杂组合
cartItemsWithDetails: (state) => {
return state.cart.map(item => {
const product = this.productMap.get(item.productId)
return {
...item,
product: product || null,
subtotal: product ? product.price * item.quantity : 0
}
})
},
// 条件组合
checkoutEnabled: (state) => {
return this.cartTotal > 0 && !!state.user && state.user.verified
}
}
})
interface Product {
id: string
name: string
price: number
}
interface CartItem {
productId: string
quantity: number
}
interface User {
id: string
email: string
verified: boolean
}
3. 类型安全的Getters
typescript
import { StoreGeneric, storeToRefs } from 'pinia'
export const useTypedGettersStore = defineStore('typedGetters', {
state: () => ({
posts: [] as Post[],
authors: new Map<string, Author>(),
currentUserId: null as string | null
}),
getters: {
// 明确的返回类型
publishedPosts: (state): Post[] => {
return state.posts.filter(post => post.status === 'published')
},
myPosts: (state): Post[] => {
if (!state.currentUserId) return []
return state.posts.filter(post => post.authorId === state.currentUserId)
},
// 复杂类型的getter
postsWithAuthorInfo: (state): PostWithAuthor[] => {
return state.posts.map(post => {
const author = state.authors.get(post.authorId)
return {
...post,
author: author || null
}
})
},
// 带参数的类型化getter
getPostsByAuthor: (state) => {
return (authorId: string): Post[] => {
return state.posts.filter(post => post.authorId === authorId)
}
}
}
})
interface Post {
id: string
title: string
content: string
authorId: string
status: 'draft' | 'published' | 'archived'
}
interface Author {
id: string
name: string
email: string
}
interface PostWithAuthor extends Post {
author: Author | null
}
总结
Getters是Pinia中强大的计算属性机制,通过本章的学习,你应该掌握:
- 基础Getters:定义和使用简单的计算属性
- 带参数的Getters:创建灵活的过滤和搜索功能
- 复杂逻辑Getters:实现统计、聚合和转换
- 性能优化:使用computed缓存和避免副作用
- 错误处理:在Getters中安全地处理异常
- 最佳实践:命名约定、组合和类型安全
Getters使我们能够以声明式的方式从状态派生数据,同时自动处理依赖追踪和缓存,是构建响应式应用的重要工具。