Vue.js 状态管理
什么是状态管理?
状态管理是指在应用中管理和共享数据的方式。当应用变得复杂时,组件之间需要共享状态,这时就需要一个集中式的状态管理方案。
为什么需要状态管理?
问题场景
组件 A
├── 组件 B
│ └── 组件 D
└── 组件 C
└── 组件 E如果组件 D 和组件 E 需要共享数据,通过 props 和 events 会很繁琐。
Vue 3 状态管理方案
1. 简单状态管理(Reactive)
对于简单应用,可以使用 reactive 创建全局状态:
javascript
// store.js
import { reactive } from 'vue'
export const store = reactive({
count: 0,
increment() {
this.count++
}
})vue
<!-- ComponentA.vue -->
<script setup>
import { store } from './store'
</script>
<template>
<div>
<p>Count: {{ store.count }}</p>
<button @click="store.increment()">增加</button>
</div>
</template>2. Pinia(推荐)
Pinia 是 Vue 3 官方推荐的状态管理库,比 Vuex 更简单、更符合 Vue 3 的设计理念。
安装 Pinia
bash
npm install pinia创建 Store
javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
const name = ref('Counter')
// getters
const doubleCount = computed(() => count.value * 2)
// actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
async function incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
count.value++
}
return {
count,
name,
doubleCount,
increment,
decrement,
incrementAsync
}
})在应用中使用
javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')在组件中使用
vue
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<div>
<h2>{{ counter.name }}</h2>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment()">增加</button>
<button @click="counter.decrement()">减少</button>
<button @click="counter.incrementAsync()">异步增加</button>
</div>
</template>实战示例:购物车 Store
javascript
// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
// State
const items = ref([])
// Getters
const totalItems = computed(() => {
return items.value.reduce((sum, item) => sum + item.quantity, 0)
})
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => {
return sum + item.price * item.quantity
}, 0)
})
const isEmpty = computed(() => items.value.length === 0)
// Actions
function addItem(product) {
const existingItem = items.value.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
items.value.push({
...product,
quantity: 1
})
}
}
function removeItem(productId) {
const index = items.value.findIndex(item => item.id === productId)
if (index > -1) {
items.value.splice(index, 1)
}
}
function updateQuantity(productId, quantity) {
const item = items.value.find(item => item.id === productId)
if (item) {
item.quantity = Math.max(1, quantity)
}
}
function clearCart() {
items.value = []
}
async function checkout() {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000))
const order = {
items: items.value,
total: totalPrice.value,
timestamp: new Date()
}
clearCart()
return order
}
return {
items,
totalItems,
totalPrice,
isEmpty,
addItem,
removeItem,
updateQuantity,
clearCart,
checkout
}
})使用购物车 Store
vue
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
const products = [
{ id: 1, name: 'iPhone 14', price: 5999 },
{ id: 2, name: 'MacBook Pro', price: 12999 },
{ id: 3, name: 'iPad Air', price: 4399 },
{ id: 4, name: 'AirPods Pro', price: 1999 }
]
async function handleCheckout() {
try {
const order = await cart.checkout()
alert(`订单已提交!总金额:¥${order.total}`)
} catch (error) {
alert('结算失败,请重试')
}
}
</script>
<template>
<div class="shopping-app">
<div class="products">
<h2>商品列表</h2>
<div v-for="product in products" :key="product.id" class="product-card">
<h3>{{ product.name }}</h3>
<p class="price">¥{{ product.price }}</p>
<button @click="cart.addItem(product)">加入购物车</button>
</div>
</div>
<div class="cart">
<h2>购物车 ({{ cart.totalItems }})</h2>
<div v-if="cart.isEmpty" class="empty">
购物车是空的
</div>
<div v-else>
<div v-for="item in cart.items" :key="item.id" class="cart-item">
<div class="item-info">
<h4>{{ item.name }}</h4>
<p>¥{{ item.price }}</p>
</div>
<div class="item-actions">
<input
type="number"
:value="item.quantity"
@input="cart.updateQuantity(item.id, Number($event.target.value))"
min="1"
/>
<button @click="cart.removeItem(item.id)" class="remove-btn">
删除
</button>
</div>
</div>
<div class="cart-footer">
<div class="total">
<strong>总计:¥{{ cart.totalPrice }}</strong>
</div>
<button @click="handleCheckout" class="checkout-btn">
结算
</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.shopping-app {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
align-content: start;
}
.product-card {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
text-align: center;
}
.price {
font-size: 20px;
font-weight: bold;
color: #42b983;
margin: 10px 0;
}
button {
padding: 8px 16px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #35a372;
}
.cart {
position: sticky;
top: 20px;
height: fit-content;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #f9f9f9;
}
.empty {
text-align: center;
color: #999;
padding: 40px 0;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 0;
border-bottom: 1px solid #ddd;
}
.item-actions {
display: flex;
gap: 10px;
align-items: center;
}
.item-actions input {
width: 60px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.remove-btn {
background-color: #f56c6c;
}
.cart-footer {
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #ddd;
}
.total {
font-size: 18px;
margin-bottom: 15px;
text-align: right;
}
.checkout-btn {
width: 100%;
padding: 12px;
font-size: 16px;
}
</style>实战示例:用户认证 Store
javascript
// stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
// State
const user = ref(null)
const token = ref(localStorage.getItem('token') || null)
const loading = ref(false)
// Getters
const isAuthenticated = computed(() => !!token.value)
const isAdmin = computed(() => user.value?.role === 'admin')
// Actions
async function login(credentials) {
loading.value = true
try {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1000))
// 模拟响应
const response = {
user: {
id: 1,
name: credentials.username,
email: `${credentials.username}@example.com`,
role: credentials.username === 'admin' ? 'admin' : 'user'
},
token: 'fake-jwt-token-' + Date.now()
}
user.value = response.user
token.value = response.token
localStorage.setItem('token', response.token)
return response
} catch (error) {
throw new Error('登录失败')
} finally {
loading.value = false
}
}
function logout() {
user.value = null
token.value = null
localStorage.removeItem('token')
}
async function fetchUser() {
if (!token.value) return
loading.value = true
try {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 500))
user.value = {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
role: 'user'
}
} catch (error) {
logout()
} finally {
loading.value = false
}
}
async function updateProfile(data) {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 1000))
user.value = { ...user.value, ...data }
} finally {
loading.value = false
}
}
return {
user,
token,
loading,
isAuthenticated,
isAdmin,
login,
logout,
fetchUser,
updateProfile
}
})使用认证 Store
vue
<script setup>
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
const loginForm = ref({
username: '',
password: ''
})
async function handleLogin() {
try {
await auth.login(loginForm.value)
alert('登录成功!')
} catch (error) {
alert(error.message)
}
}
function handleLogout() {
auth.logout()
alert('已退出登录')
}
</script>
<template>
<div class="auth-demo">
<div v-if="!auth.isAuthenticated" class="login-form">
<h2>登录</h2>
<form @submit.prevent="handleLogin">
<input
v-model="loginForm.username"
placeholder="用户名"
required
/>
<input
v-model="loginForm.password"
type="password"
placeholder="密码"
required
/>
<button type="submit" :disabled="auth.loading">
{{ auth.loading ? '登录中...' : '登录' }}
</button>
</form>
<p class="hint">提示:输入 "admin" 作为用户名可获得管理员权限</p>
</div>
<div v-else class="user-profile">
<h2>欢迎,{{ auth.user.name }}!</h2>
<div class="user-info">
<p><strong>邮箱:</strong>{{ auth.user.email }}</p>
<p><strong>角色:</strong>{{ auth.user.role }}</p>
<span v-if="auth.isAdmin" class="badge">管理员</span>
</div>
<div v-if="auth.isAdmin" class="admin-panel">
<h3>🔧 管理员面板</h3>
<p>只有管理员可以看到这个面板</p>
</div>
<button @click="handleLogout" class="logout-btn">退出登录</button>
</div>
</div>
</template>
<style scoped>
.auth-demo {
max-width: 500px;
margin: 20px auto;
padding: 30px;
border: 1px solid #ddd;
border-radius: 8px;
}
.login-form input {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.login-form button {
width: 100%;
padding: 12px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.login-form button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.hint {
margin-top: 15px;
font-size: 14px;
color: #666;
text-align: center;
}
.user-info {
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
margin: 20px 0;
}
.badge {
display: inline-block;
padding: 4px 12px;
background-color: #42b983;
color: white;
border-radius: 12px;
font-size: 12px;
}
.admin-panel {
padding: 20px;
background-color: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
margin: 20px 0;
}
.logout-btn {
width: 100%;
padding: 12px;
background-color: #f56c6c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
</style>Store 之间的组合
javascript
// stores/user.js
import { defineStore } from 'pinia'
import { useAuthStore } from './auth'
export const useUserStore = defineStore('user', () => {
const auth = useAuthStore()
async function updateSettings(settings) {
if (!auth.isAuthenticated) {
throw new Error('请先登录')
}
// 更新设置逻辑
}
return {
updateSettings
}
})Pinia 插件
javascript
// plugins/piniaLogger.js
export function piniaLogger(context) {
context.store.$subscribe((mutation, state) => {
console.log(`[${context.store.$id}] ${mutation.type}`, state)
})
}
// main.js
import { createPinia } from 'pinia'
import { piniaLogger } from './plugins/piniaLogger'
const pinia = createPinia()
pinia.use(piniaLogger)持久化存储
javascript
// stores/settings.js
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
export const useSettingsStore = defineStore('settings', () => {
const theme = ref(localStorage.getItem('theme') || 'light')
const language = ref(localStorage.getItem('language') || 'zh-CN')
// 监听变化并保存到 localStorage
watch(theme, (newTheme) => {
localStorage.setItem('theme', newTheme)
})
watch(language, (newLang) => {
localStorage.setItem('language', newLang)
})
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
return {
theme,
language,
toggleTheme
}
})总结
- 简单应用:使用
reactive创建全局状态 - 复杂应用:使用 Pinia 进行状态管理
- Pinia 优势:
- 更简单的 API
- 完整的 TypeScript 支持
- 更好的开发工具支持
- 模块化设计
- 支持插件系统
- 最佳实践:
- 按功能模块划分 Store
- 使用组合式 API 风格
- 合理使用 getters 缓存计算结果
- 异步操作放在 actions 中
下一步
接下来学习 动画和过渡,了解如何为应用添加动画效果。