Skip to content

状态管理

23:52

概述

项目使用 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)

最佳实践

  1. Store 命名 - 使用 use[PascalCase]Store 命名规范
  2. 单一职责 - 每个 Store 只管理一个业务模块的状态
  3. 不在 Store 外修改状态 - 所有状态修改都应通过 action
  4. 利用持久化 - 对需要长期保存的数据启用持久化
  5. 类型安全 - 使用 TypeScript 定义状态类型
  6. 避免过度嵌套 - 保持状态结构的扁平化
  7. 分离异步逻辑 - 将 API 调用放在 action 中

常见问题

Store 中的响应性问题

确保在 Setup Store 中使用 ref() 定义状态,而不是普通对象。

持久化数据不更新

检查:

  1. 确认持久化插件已正确配置
  2. 确认状态修改是通过 action 进行的
  3. 在某些情况下可能需要手动触发持久化

跨页面通信

使用 Store 的 $subscribe 方法监听变化,或在 action 中调用其他 Store 的 action。

相关文档