Pinia是Vue.js的官方状态管理库,是Vuex的继任者。本章将详细介绍Pinia的使用方法、状态管理模式和最佳实践。
6.1 状态管理基础
什么是状态管理
状态管理是指在应用中统一管理和维护数据状态的模式。当应用变得复杂时,组件间的数据共享和状态同步变得困难,这时就需要状态管理库。
安装和配置Pinia
# 安装Pinia
npm install pinia
# 或使用yarn
yarn add pinia
# 或使用pnpm
pnpm add pinia
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.mount('#app')
6.2 定义Store
基本Store定义
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 状态
state: () => ({
count: 0,
name: 'Counter Store',
history: []
}),
// 计算属性
getters: {
doubleCount: (state) => state.count * 2,
// 带参数的getter
countPlusN: (state) => {
return (n) => state.count + n
},
// 访问其他getter
doubleCountPlusOne() {
return this.doubleCount + 1
},
// 访问其他store
otherStoreValue() {
const otherStore = useOtherStore()
return otherStore.value
}
},
// 动作
actions: {
increment() {
this.count++
this.history.push({ action: 'increment', timestamp: Date.now() })
},
decrement() {
this.count--
this.history.push({ action: 'decrement', timestamp: Date.now() })
},
incrementBy(amount) {
this.count += amount
this.history.push({
action: 'incrementBy',
amount,
timestamp: Date.now()
})
},
async fetchData() {
try {
// 模拟API调用
const response = await fetch('/api/counter')
const data = await response.json()
this.count = data.count
} catch (error) {
console.error('Failed to fetch data:', error)
}
},
reset() {
this.count = 0
this.history = []
},
// 批量更新
$patch(partialState) {
// 这是内置方法,这里只是展示用法
}
}
})
Composition API风格的Store
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态 (相当于state)
const user = ref(null)
const isLoggedIn = ref(false)
const preferences = ref({
theme: 'light',
language: 'zh-CN',
notifications: true
})
const loginHistory = ref([])
// 计算属性 (相当于getters)
const userName = computed(() => user.value?.name || 'Guest')
const userInitials = computed(() => {
if (!user.value?.name) return 'G'
return user.value.name
.split(' ')
.map(word => word[0])
.join('')
.toUpperCase()
})
const isAdmin = computed(() => user.value?.role === 'admin')
const canEdit = computed(() => isLoggedIn.value && user.value?.permissions?.includes('edit'))
// 动作 (相当于actions)
async function login(credentials) {
try {
// 模拟登录API
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
if (!response.ok) {
throw new Error('Login failed')
}
const userData = await response.json()
user.value = userData.user
isLoggedIn.value = true
// 记录登录历史
loginHistory.value.push({
timestamp: Date.now(),
ip: userData.ip,
userAgent: navigator.userAgent
})
// 保存到localStorage
localStorage.setItem('user', JSON.stringify(userData.user))
localStorage.setItem('token', userData.token)
return userData
} catch (error) {
console.error('Login error:', error)
throw error
}
}
function logout() {
user.value = null
isLoggedIn.value = false
// 清除localStorage
localStorage.removeItem('user')
localStorage.removeItem('token')
}
function updateProfile(profileData) {
if (user.value) {
user.value = { ...user.value, ...profileData }
localStorage.setItem('user', JSON.stringify(user.value))
}
}
function updatePreferences(newPreferences) {
preferences.value = { ...preferences.value, ...newPreferences }
localStorage.setItem('preferences', JSON.stringify(preferences.value))
}
function initializeFromStorage() {
const storedUser = localStorage.getItem('user')
const storedPreferences = localStorage.getItem('preferences')
const token = localStorage.getItem('token')
if (storedUser && token) {
user.value = JSON.parse(storedUser)
isLoggedIn.value = true
}
if (storedPreferences) {
preferences.value = JSON.parse(storedPreferences)
}
}
// 返回要暴露的内容
return {
// 状态
user,
isLoggedIn,
preferences,
loginHistory,
// 计算属性
userName,
userInitials,
isAdmin,
canEdit,
// 动作
login,
logout,
updateProfile,
updatePreferences,
initializeFromStorage
}
})
6.3 在组件中使用Store
基本使用
<!-- components/Counter.vue -->
<template>
<div class="counter">
<div class="counter-display">
<h2>计数器</h2>
<div class="count-value">{{ count }}</div>
<div class="count-info">
<p>双倍值: {{ doubleCount }}</p>
<p>双倍值+1: {{ doubleCountPlusOne }}</p>
<p>当前值+10: {{ countPlusN(10) }}</p>
</div>
</div>
<div class="counter-controls">
<button @click="decrement" class="btn btn-danger">-</button>
<button @click="increment" class="btn btn-primary">+</button>
<button @click="incrementBy(5)" class="btn btn-secondary">+5</button>
<button @click="reset" class="btn btn-warning">重置</button>
</div>
<div class="counter-history">
<h3>操作历史</h3>
<div class="history-list">
<div
v-for="(item, index) in history"
:key="index"
class="history-item"
>
<span class="action">{{ item.action }}</span>
<span v-if="item.amount" class="amount">({{ item.amount }})</span>
<span class="timestamp">{{ formatTime(item.timestamp) }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
export default {
name: 'Counter',
setup() {
const counterStore = useCounterStore()
// 使用storeToRefs保持响应性
const { count, doubleCount, doubleCountPlusOne, history } = storeToRefs(counterStore)
// 直接解构actions(不需要storeToRefs)
const { increment, decrement, incrementBy, reset, countPlusN } = counterStore
function formatTime(timestamp) {
return new Date(timestamp).toLocaleTimeString()
}
return {
count,
doubleCount,
doubleCountPlusOne,
history,
increment,
decrement,
incrementBy,
reset,
countPlusN,
formatTime
}
}
}
</script>
<style scoped>
.counter {
max-width: 400px;
margin: 0 auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.counter-display {
text-align: center;
margin-bottom: 2rem;
}
.counter-display h2 {
color: #2c3e50;
margin-bottom: 1rem;
}
.count-value {
font-size: 3rem;
font-weight: bold;
color: #3498db;
margin-bottom: 1rem;
}
.count-info {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.count-info p {
margin: 0.5rem 0;
color: #666;
}
.counter-controls {
display: flex;
gap: 0.5rem;
justify-content: center;
margin-bottom: 2rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.3s;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.btn-warning {
background: #f39c12;
color: white;
}
.btn-warning:hover {
background: #e67e22;
}
.counter-history {
border-top: 1px solid #dee2e6;
padding-top: 1rem;
}
.counter-history h3 {
color: #2c3e50;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.history-list {
max-height: 200px;
overflow-y: auto;
}
.history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid #f1f1f1;
font-size: 0.9rem;
}
.history-item:last-child {
border-bottom: none;
}
.action {
font-weight: 500;
color: #2c3e50;
}
.amount {
color: #3498db;
}
.timestamp {
color: #7f8c8d;
font-size: 0.8rem;
}
</style>
用户管理组件
<!-- components/UserProfile.vue -->
<template>
<div class="user-profile">
<!-- 未登录状态 -->
<div v-if="!isLoggedIn" class="login-form">
<h2>用户登录</h2>
<form @submit.prevent="handleLogin">
<div class="form-group">
<label for="email">邮箱:</label>
<input
id="email"
v-model="loginForm.email"
type="email"
required
class="form-control"
>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input
id="password"
v-model="loginForm.password"
type="password"
required
class="form-control"
>
</div>
<button type="submit" :disabled="isLoading" class="btn btn-primary">
{{ isLoading ? '登录中...' : '登录' }}
</button>
</form>
<div v-if="loginError" class="error-message">
{{ loginError }}
</div>
</div>
<!-- 已登录状态 -->
<div v-else class="user-info">
<div class="user-header">
<div class="user-avatar">
<img v-if="user.avatar" :src="user.avatar" :alt="userName">
<div v-else class="avatar-placeholder">
{{ userInitials }}
</div>
</div>
<div class="user-details">
<h2>{{ userName }}</h2>
<p class="user-email">{{ user.email }}</p>
<span class="user-role" :class="`role-${user.role}`">
{{ getRoleLabel(user.role) }}
</span>
</div>
<button @click="logout" class="btn btn-secondary">退出登录</button>
</div>
<!-- 用户偏好设置 -->
<div class="preferences-section">
<h3>偏好设置</h3>
<div class="preference-item">
<label>主题:</label>
<select v-model="localPreferences.theme" @change="updatePreferences">
<option value="light">浅色</option>
<option value="dark">深色</option>
<option value="auto">自动</option>
</select>
</div>
<div class="preference-item">
<label>语言:</label>
<select v-model="localPreferences.language" @change="updatePreferences">
<option value="zh-CN">中文</option>
<option value="en-US">English</option>
<option value="ja-JP">日本語</option>
</select>
</div>
<div class="preference-item">
<label>
<input
v-model="localPreferences.notifications"
type="checkbox"
@change="updatePreferences"
>
接收通知
</label>
</div>
</div>
<!-- 权限信息 -->
<div class="permissions-section">
<h3>权限信息</h3>
<div class="permission-list">
<div class="permission-item">
<span>管理员权限:</span>
<span :class="isAdmin ? 'status-yes' : 'status-no'">
{{ isAdmin ? '是' : '否' }}
</span>
</div>
<div class="permission-item">
<span>编辑权限:</span>
<span :class="canEdit ? 'status-yes' : 'status-no'">
{{ canEdit ? '是' : '否' }}
</span>
</div>
</div>
</div>
<!-- 登录历史 -->
<div class="login-history">
<h3>登录历史</h3>
<div class="history-list">
<div
v-for="(login, index) in loginHistory.slice(-5)"
:key="index"
class="history-item"
>
<span class="login-time">
{{ formatDateTime(login.timestamp) }}
</span>
<span class="login-ip">{{ login.ip }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, reactive, computed } from 'vue'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
export default {
name: 'UserProfile',
setup() {
const userStore = useUserStore()
// 从store获取响应式数据
const {
user,
isLoggedIn,
preferences,
loginHistory,
userName,
userInitials,
isAdmin,
canEdit
} = storeToRefs(userStore)
// 获取actions
const { login, logout, updatePreferences: storeUpdatePreferences } = userStore
// 本地状态
const isLoading = ref(false)
const loginError = ref('')
const loginForm = reactive({
email: '',
password: ''
})
// 本地偏好设置(用于双向绑定)
const localPreferences = reactive({ ...preferences.value })
// 监听偏好设置变化
function updatePreferences() {
storeUpdatePreferences(localPreferences)
}
// 处理登录
async function handleLogin() {
isLoading.value = true
loginError.value = ''
try {
await login(loginForm)
// 登录成功,重置表单
loginForm.email = ''
loginForm.password = ''
} catch (error) {
loginError.value = error.message || '登录失败,请重试'
} finally {
isLoading.value = false
}
}
// 格式化日期时间
function formatDateTime(timestamp) {
return new Date(timestamp).toLocaleString('zh-CN')
}
// 获取角色标签
function getRoleLabel(role) {
const labels = {
admin: '管理员',
editor: '编辑者',
user: '普通用户',
guest: '访客'
}
return labels[role] || role
}
// 初始化
userStore.initializeFromStorage()
return {
// 状态
user,
isLoggedIn,
preferences,
loginHistory,
userName,
userInitials,
isAdmin,
canEdit,
isLoading,
loginError,
loginForm,
localPreferences,
// 方法
handleLogin,
logout,
updatePreferences,
formatDateTime,
getRoleLabel
}
}
}
</script>
<style scoped>
.user-profile {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.login-form {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.login-form h2 {
color: #2c3e50;
margin-bottom: 1.5rem;
text-align: center;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #555;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.form-control:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s;
}
.btn-primary {
background: #3498db;
color: white;
width: 100%;
}
.btn-primary:hover:not(:disabled) {
background: #2980b9;
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: #95a5a6;
color: white;
}
.btn-secondary:hover {
background: #7f8c8d;
}
.error-message {
color: #e74c3c;
margin-top: 1rem;
padding: 0.75rem;
background: #fdf2f2;
border: 1px solid #fecaca;
border-radius: 4px;
}
.user-info {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.user-header {
display: flex;
align-items: center;
padding: 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.user-avatar {
margin-right: 1.5rem;
}
.user-avatar img {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
.avatar-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: bold;
}
.user-details {
flex: 1;
}
.user-details h2 {
margin: 0 0 0.5rem 0;
}
.user-email {
margin: 0 0 0.5rem 0;
opacity: 0.9;
}
.user-role {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.role-admin {
background: #e74c3c;
}
.role-editor {
background: #f39c12;
}
.role-user {
background: #3498db;
}
.preferences-section,
.permissions-section,
.login-history {
padding: 1.5rem 2rem;
border-bottom: 1px solid #f1f1f1;
}
.preferences-section:last-child,
.permissions-section:last-child,
.login-history:last-child {
border-bottom: none;
}
.preferences-section h3,
.permissions-section h3,
.login-history h3 {
color: #2c3e50;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.preference-item {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.preference-item label {
min-width: 80px;
margin-right: 1rem;
margin-bottom: 0;
}
.preference-item select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.permission-list {
display: grid;
gap: 0.5rem;
}
.permission-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 4px;
}
.status-yes {
color: #27ae60;
font-weight: 500;
}
.status-no {
color: #e74c3c;
font-weight: 500;
}
.history-list {
display: grid;
gap: 0.5rem;
}
.history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 4px;
font-size: 0.9rem;
}
.login-time {
font-weight: 500;
color: #2c3e50;
}
.login-ip {
color: #7f8c8d;
font-family: monospace;
}
@media (max-width: 768px) {
.user-header {
flex-direction: column;
text-align: center;
}
.user-avatar {
margin-right: 0;
margin-bottom: 1rem;
}
.preference-item {
flex-direction: column;
align-items: flex-start;
}
.preference-item label {
margin-bottom: 0.5rem;
}
.history-item {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
}
</style>
6.4 Store组合和模块化
购物车Store示例
// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useProductStore } from './product'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
coupon: null,
shippingMethod: 'standard'
}),
getters: {
// 购物车商品数量
itemCount: (state) => {
return state.items.reduce((total, item) => total + item.quantity, 0)
},
// 购物车总价
subtotal: (state) => {
return state.items.reduce((total, item) => {
return total + (item.price * item.quantity)
}, 0)
},
// 折扣金额
discountAmount() {
if (!this.coupon) return 0
if (this.coupon.type === 'percentage') {
return this.subtotal * (this.coupon.value / 100)
} else if (this.coupon.type === 'fixed') {
return Math.min(this.coupon.value, this.subtotal)
}
return 0
},
// 运费
shippingCost() {
const costs = {
standard: 10,
express: 25,
overnight: 50
}
// 满99免运费
if (this.subtotal >= 99) return 0
return costs[this.shippingMethod] || 0
},
// 最终总价
total() {
return this.subtotal - this.discountAmount + this.shippingCost
},
// 是否为空购物车
isEmpty: (state) => state.items.length === 0,
// 获取特定商品
getItemById: (state) => {
return (productId) => state.items.find(item => item.id === productId)
}
},
actions: {
// 添加商品到购物车
addItem(product, quantity = 1) {
const existingItem = this.items.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity += quantity
} else {
this.items.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity,
addedAt: Date.now()
})
}
this.saveToStorage()
},
// 更新商品数量
updateQuantity(productId, quantity) {
const item = this.items.find(item => item.id === productId)
if (item) {
if (quantity <= 0) {
this.removeItem(productId)
} else {
item.quantity = quantity
this.saveToStorage()
}
}
},
// 移除商品
removeItem(productId) {
const index = this.items.findIndex(item => item.id === productId)
if (index > -1) {
this.items.splice(index, 1)
this.saveToStorage()
}
},
// 清空购物车
clear() {
this.items = []
this.coupon = null
this.saveToStorage()
},
// 应用优惠券
applyCoupon(couponCode) {
// 模拟优惠券验证
const validCoupons = {
'SAVE10': { type: 'percentage', value: 10, minAmount: 50 },
'SAVE20': { type: 'fixed', value: 20, minAmount: 100 },
'FREESHIP': { type: 'shipping', value: 0, minAmount: 0 }
}
const coupon = validCoupons[couponCode.toUpperCase()]
if (!coupon) {
throw new Error('无效的优惠券代码')
}
if (this.subtotal < coupon.minAmount) {
throw new Error(`订单金额需满${coupon.minAmount}元才能使用此优惠券`)
}
this.coupon = { ...coupon, code: couponCode.toUpperCase() }
this.saveToStorage()
},
// 移除优惠券
removeCoupon() {
this.coupon = null
this.saveToStorage()
},
// 设置配送方式
setShippingMethod(method) {
this.shippingMethod = method
this.saveToStorage()
},
// 结算
async checkout() {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
throw new Error('请先登录')
}
if (this.isEmpty) {
throw new Error('购物车为空')
}
try {
// 模拟结算API调用
const orderData = {
items: this.items,
subtotal: this.subtotal,
discount: this.discountAmount,
shipping: this.shippingCost,
total: this.total,
coupon: this.coupon,
shippingMethod: this.shippingMethod,
userId: userStore.user.id
}
const response = await fetch('/api/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(orderData)
})
if (!response.ok) {
throw new Error('结算失败')
}
const result = await response.json()
// 清空购物车
this.clear()
return result
} catch (error) {
console.error('Checkout error:', error)
throw error
}
},
// 保存到本地存储
saveToStorage() {
localStorage.setItem('cart', JSON.stringify({
items: this.items,
coupon: this.coupon,
shippingMethod: this.shippingMethod
}))
},
// 从本地存储加载
loadFromStorage() {
const saved = localStorage.getItem('cart')
if (saved) {
const data = JSON.parse(saved)
this.items = data.items || []
this.coupon = data.coupon || null
this.shippingMethod = data.shippingMethod || 'standard'
}
}
}
})
产品Store示例
// stores/product.js
import { defineStore } from 'pinia'
export const useProductStore = defineStore('product', {
state: () => ({
products: [],
categories: [],
currentProduct: null,
loading: false,
error: null,
filters: {
category: '',
priceRange: [0, 1000],
sortBy: 'name',
sortOrder: 'asc'
},
pagination: {
page: 1,
pageSize: 12,
total: 0
}
}),
getters: {
// 过滤后的产品
filteredProducts() {
let filtered = [...this.products]
// 按分类过滤
if (this.filters.category) {
filtered = filtered.filter(product =>
product.category === this.filters.category
)
}
// 按价格范围过滤
filtered = filtered.filter(product =>
product.price >= this.filters.priceRange[0] &&
product.price <= this.filters.priceRange[1]
)
// 排序
filtered.sort((a, b) => {
const aValue = a[this.filters.sortBy]
const bValue = b[this.filters.sortBy]
if (this.filters.sortOrder === 'asc') {
return aValue > bValue ? 1 : -1
} else {
return aValue < bValue ? 1 : -1
}
})
return filtered
},
// 分页后的产品
paginatedProducts() {
const start = (this.pagination.page - 1) * this.pagination.pageSize
const end = start + this.pagination.pageSize
return this.filteredProducts.slice(start, end)
},
// 总页数
totalPages() {
return Math.ceil(this.filteredProducts.length / this.pagination.pageSize)
},
// 按分类分组的产品
productsByCategory() {
const grouped = {}
this.products.forEach(product => {
if (!grouped[product.category]) {
grouped[product.category] = []
}
grouped[product.category].push(product)
})
return grouped
},
// 获取特定产品
getProductById: (state) => {
return (id) => state.products.find(product => product.id === id)
}
},
actions: {
// 获取产品列表
async fetchProducts() {
this.loading = true
this.error = null
try {
const response = await fetch('/api/products')
if (!response.ok) {
throw new Error('Failed to fetch products')
}
const data = await response.json()
this.products = data.products
this.pagination.total = data.total
} catch (error) {
this.error = error.message
console.error('Error fetching products:', error)
} finally {
this.loading = false
}
},
// 获取产品分类
async fetchCategories() {
try {
const response = await fetch('/api/categories')
if (!response.ok) {
throw new Error('Failed to fetch categories')
}
const data = await response.json()
this.categories = data
} catch (error) {
console.error('Error fetching categories:', error)
}
},
// 获取单个产品详情
async fetchProduct(id) {
this.loading = true
this.error = null
try {
const response = await fetch(`/api/products/${id}`)
if (!response.ok) {
throw new Error('Product not found')
}
const product = await response.json()
this.currentProduct = product
return product
} catch (error) {
this.error = error.message
console.error('Error fetching product:', error)
throw error
} finally {
this.loading = false
}
},
// 搜索产品
async searchProducts(query) {
this.loading = true
this.error = null
try {
const response = await fetch(`/api/products/search?q=${encodeURIComponent(query)}`)
if (!response.ok) {
throw new Error('Search failed')
}
const data = await response.json()
this.products = data.products
this.pagination.total = data.total
this.pagination.page = 1 // 重置到第一页
} catch (error) {
this.error = error.message
console.error('Error searching products:', error)
} finally {
this.loading = false
}
},
// 更新过滤器
updateFilters(newFilters) {
this.filters = { ...this.filters, ...newFilters }
this.pagination.page = 1 // 重置到第一页
},
// 更新分页
updatePagination(newPagination) {
this.pagination = { ...this.pagination, ...newPagination }
},
// 重置过滤器
resetFilters() {
this.filters = {
category: '',
priceRange: [0, 1000],
sortBy: 'name',
sortOrder: 'asc'
}
this.pagination.page = 1
}
}
})
6.5 Pinia插件和持久化
持久化插件
// plugins/persistence.js
export function createPersistencePlugin(options = {}) {
return (context) => {
const { store, options: storeOptions } = context
// 检查是否启用持久化
if (!storeOptions.persist) return
const config = {
key: storeOptions.persist.key || store.$id,
storage: storeOptions.persist.storage || localStorage,
paths: storeOptions.persist.paths || null,
...options
}
// 从存储中恢复状态
function restore() {
try {
const saved = config.storage.getItem(config.key)
if (saved) {
const data = JSON.parse(saved)
if (config.paths) {
// 只恢复指定的路径
config.paths.forEach(path => {
if (data[path] !== undefined) {
store.$patch({ [path]: data[path] })
}
})
} else {
// 恢复整个状态
store.$patch(data)
}
}
} catch (error) {
console.error('Failed to restore state:', error)
}
}
// 保存状态到存储
function persist() {
try {
let dataToSave
if (config.paths) {
// 只保存指定的路径
dataToSave = {}
config.paths.forEach(path => {
dataToSave[path] = store.$state[path]
})
} else {
// 保存整个状态
dataToSave = store.$state
}
config.storage.setItem(config.key, JSON.stringify(dataToSave))
} catch (error) {
console.error('Failed to persist state:', error)
}
}
// 恢复状态
restore()
// 监听状态变化并持久化
store.$subscribe((mutation, state) => {
persist()
})
}
}
// main.js - 使用持久化插件
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createPersistencePlugin } from './plugins/persistence'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
// 添加持久化插件
pinia.use(createPersistencePlugin())
app.use(pinia)
app.mount('#app')
// stores/settings.js - 使用持久化
import { defineStore } from 'pinia'
export const useSettingsStore = defineStore('settings', {
state: () => ({
theme: 'light',
language: 'zh-CN',
notifications: true,
autoSave: true,
tempData: null // 这个不会被持久化
}),
actions: {
updateTheme(theme) {
this.theme = theme
},
updateLanguage(language) {
this.language = language
},
toggleNotifications() {
this.notifications = !this.notifications
}
}
}, {
// 持久化配置
persist: {
key: 'app-settings',
storage: localStorage,
paths: ['theme', 'language', 'notifications', 'autoSave'] // 只持久化这些字段
}
})
本章小结
本章我们学习了Pinia状态管理的核心概念:
- Store定义:Options API和Composition API两种风格
- 状态管理:state、getters、actions的使用
- 组件集成:在组件中使用store和保持响应性
- Store组合:多个store之间的协作
- 插件系统:持久化和其他扩展功能
下一章预告
下一章我们将学习Vue.js的高级特性,包括: - 自定义指令和插件开发 - 组合式API深入应用 - 性能优化技巧 - 测试策略
练习题
基础练习
计数器Store:
- 创建一个计数器store
- 实现增加、减少、重置功能
- 添加操作历史记录
用户管理Store:
- 实现用户登录/登出
- 添加用户偏好设置
- 实现数据持久化
进阶练习
购物车系统:
- 创建完整的购物车功能
- 实现优惠券系统
- 添加结算流程
状态持久化:
- 实现自定义持久化插件
- 支持选择性持久化
- 添加数据迁移功能
提示:状态管理是大型应用的核心,合理的状态设计能大大提升开发效率和代码可维护性。