状态管理
13:16概述
项目使用 Pinia 作为状态管理库,并集成了 pinia-plugin-persistedstate 插件,实现状态的自动持久化。Pinia 是 Vue 3 官方推荐的状态管理库,提供了比 Vuex 更简洁的 API。
项目特性
- 🎯 Setup Store - 使用现代的 Composition API 风格
- 💾 自动持久化 - 状态自动保存到本地存储
- 🔄 响应式 - 完整的 Vue 3 响应式系统支持
- 📦 模块化 - 支持多个 Store 的模块化管理
- ⚡ 零 Boilerplate - 相比 Vuex 减少了大量的样板代码
Pinia 初始化
Store 初始化配置
在 src/stores/index.ts 中:
typescript
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'
const pinia = createPinia()
// 配置数据持久化
pinia.use(
createPersistedState({
storage: {
getItem: uni.getStorageSync, // 获取本地存储
setItem: uni.setStorageSync, // 设置本地存储
},
}),
)
export default pinia创建 Store
基础 Store 示例
创建一个计数器 Store(src/stores/counter.ts):
typescript
import { defineStore } from 'pinia'
import { ref } from 'vue'
// defineStore 的第一个参数是 store 的唯一 ID
export const useCounterStore = defineStore('counter', () => {
// 定义状态
const count = ref(0)
// 定义 action(修改状态的方法)
function increment() {
count.value++
}
function decrement() {
if (count.value > 0) {
count.value--
}
}
function reset() {
count.value = 0
}
function setCount(n: number) {
count.value = n
}
// 返回需要暴露的状态和方法
return { count, increment, decrement, reset, setCount }
})用户信息 Store 示例
创建一个用户 Store(src/stores/user.ts):
typescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 定义状态
const userInfo = ref({
id: '',
name: '',
avatar: '',
email: '',
})
const isLogin = ref(false)
const token = ref('')
// 计算属性
const displayName = computed(() => {
return userInfo.value.name || '游客'
})
// 登录 action
function login(credentials: any) {
return new Promise((resolve, reject) => {
// 调用登录 API
// const res = await request.post('/api/login', credentials)
// 设置状态
isLogin.value = true
token.value = 'mock_token_123'
userInfo.value = {
id: '1',
name: '张三',
avatar: 'https://example.com/avatar.jpg',
email: 'zhangsan@example.com',
}
resolve({ success: true })
})
}
// 登出 action
function logout() {
isLogin.value = false
token.value = ''
userInfo.value = {
id: '',
name: '',
avatar: '',
email: '',
}
}
// 更新用户信息
function updateUserInfo(info: any) {
userInfo.value = { ...userInfo.value, ...info }
}
// 设置 token
function setToken(newToken: string) {
token.value = newToken
}
return {
userInfo,
isLogin,
token,
displayName,
login,
logout,
updateUserInfo,
setToken,
}
}, {
persist: true // 启用持久化
})在组件中使用 Store
在 Setup 中使用
vue
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// 访问状态
const count = computed(() => counterStore.count)
// 调用 action
const handleIncrement = () => {
counterStore.increment()
}
</script>
<template>
<view class="counter">
<view class="count">{{ count }}</view>
<u-button @click="handleIncrement">增加</u-button>
<u-button @click="counterStore.decrement">减少</u-button>
<u-button @click="counterStore.reset">重置</u-button>
</view>
</template>直接使用状态
vue
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
<template>
<!-- 无需 computed,直接使用 -->
<view class="count">{{ counterStore.count }}</view>
</template>使用解构(需要注意响应性)
typescript
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
// 使用 storeToRefs 保持响应性
const { count } = storeToRefs(counterStore)
const { increment, decrement } = counterStore高级用法
在组合函数中使用 Store
创建 src/hooks/useUserAuth.ts:
typescript
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
export function useUserAuth() {
const userStore = useUserStore()
const isLogin = computed(() => userStore.isLogin)
const userName = computed(() => userStore.displayName)
const login = async (email: string, password: string) => {
try {
await userStore.login({ email, password })
return true
} catch (error) {
console.error('登录失败:', error)
return false
}
}
const logout = () => {
userStore.logout()
}
return {
isLogin,
userName,
login,
logout,
}
}在组件中使用:
vue
<script setup lang="ts">
import { useUserAuth } from '@/composables/useUserAuth'
const { isLogin, userName, login, logout } = useUserAuth()
const handleLogin = async () => {
const success = await login('user@example.com', 'password')
if (success) {
console.log('登录成功')
}
}
</script>
<template>
<view v-if="isLogin" class="user-panel">
<text>欢迎 {{ userName }}</text>
<u-button @click="logout">登出</u-button>
</view>
<view v-else class="login-panel">
<u-button @click="handleLogin">登录</u-button>
</view>
</template>Store 之间的通信
typescript
// stores/notifications.ts
export const useNotificationsStore = defineStore('notifications', () => {
const notifications = ref([])
function addNotification(message: string, type: string = 'info') {
notifications.value.push({
id: Date.now(),
message,
type,
})
}
return { notifications, addNotification }
})
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const notificationsStore = useNotificationsStore()
async function login(credentials: any) {
try {
// 登录逻辑
notificationsStore.addNotification('登录成功', 'success')
} catch (error) {
notificationsStore.addNotification('登录失败', 'error')
}
}
return { login }
})监听状态变化
typescript
import { watch } from 'vue'
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// 监听 count 的变化
watch(
() => counterStore.count,
(newCount) => {
console.log('count 更新为:', newCount)
}
)
// 订阅整个 store 的变化
counterStore.$subscribe((mutation, state) => {
console.log('Store 变化:', mutation, state)
})Getter(计算属性)
虽然 Setup Store 中可以使用 computed,但也可以定义 getter:
typescript
export const useStoreWithGetter = defineStore('example', () => {
const list = ref([1, 2, 3, 4, 5])
const evenNumbers = computed(() => {
return list.value.filter(n => n % 2 === 0)
})
return { list, evenNumbers }
})异步 Action
typescript
export const usePostStore = defineStore('post', () => {
const posts = ref([])
const loading = ref(false)
async function fetchPosts() {
loading.value = true
try {
// const { request } = useHttp()
// const data = await request.get('/api/posts')
// posts.value = data
posts.value = [
{ id: 1, title: '文章 1' },
{ id: 2, title: '文章 2' },
]
} finally {
loading.value = false
}
}
async function createPost(title: string) {
// const { request } = useHttp()
// const newPost = await request.post('/api/posts', { title })
const newPost = { id: Date.now(), title }
posts.value.push(newPost)
return newPost
}
return {
posts,
loading,
fetchPosts,
createPost,
}
})持久化配置
持久化特定字段
默认情况下,所有状态都会被持久化。如果只想持久化部分字段:
typescript
export const useUserStore = defineStore('user', () => {
const token = ref('')
const tempData = ref('') // 不持久化
return { token, tempData }
}, {
persist: {
paths: ['token'] // 只持久化 token
}
})自定义持久化逻辑
typescript
export const useCustomStore = defineStore('custom', () => {
// ...
}, {
persist: {
storage: localStorage, // 使用 localStorage 而不是 sessionStorage
key: 'my_custom_store' // 自定义存储 key
}
})调试
使用 Vue DevTools
安装 Vue DevTools 扩展,可以:
- 查看 Store 的状态
- 追踪状态变化
- 时间旅行调试
打印 Store 信息
typescript
const counterStore = useCounterStore()
// 查看完整的 store 对象
console.log(counterStore.$state)
// 查看所有 action
console.log(counterStore.$getters)
// 查看订阅信息
console.log(counterStore.$id)最佳实践
- Store 命名 - 使用
use[PascalCase]Store命名规范 - 单一职责 - 每个 Store 只管理一个业务模块的状态
- 不在 Store 外修改状态 - 所有状态修改都应通过 action
- 利用持久化 - 对需要长期保存的数据启用持久化
- 类型安全 - 使用 TypeScript 定义状态类型
- 避免过度嵌套 - 保持状态结构的扁平化
- 分离异步逻辑 - 将 API 调用放在 action 中
常见问题
Store 中的响应性问题
确保在 Setup Store 中使用 ref() 定义状态,而不是普通对象。
持久化数据不更新
检查:
- 确认持久化插件已正确配置
- 确认状态修改是通过 action 进行的
- 在某些情况下可能需要手动触发持久化
跨页面通信
使用 Store 的 $subscribe 方法监听变化,或在 action 中调用其他 Store 的 action。
