uni-app 状态管理

章节介绍

在复杂的应用中,状态管理是一个重要的挑战。当应用规模扩大,组件层级变深时,组件间的通信会变得复杂和难以维护。uni-app 集成了 Vuex 作为官方推荐的状态管理方案,帮助开发者集中管理应用状态,实现组件间的高效通信。本章节将详细介绍 uni-app 中的状态管理方法,包括 Vuex 的基本使用、状态管理的最佳实践以及模块化状态管理的实现,帮助你构建可维护性更高的应用。

核心知识点讲解

1. Vuex 概述

Vuex 是一个专为 Vue.js 应用设计的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex 的核心概念包括:

  • State:存储应用状态的对象
  • Getter:从 State 中派生出的计算属性
  • Mutation:修改 State 的唯一方法,必须是同步函数
  • Action:处理异步操作,可以提交 Mutation
  • Module:将 Store 分割成模块化的结构

2. Vuex 基本使用

安装和配置 Vuex

在 uni-app 项目中,Vuex 已经内置,无需单独安装。你只需要创建 Store 实例并配置即可。

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    // 状态
  },
  mutations: {
    // 修改状态的方法
  },
  actions: {
    // 异步操作
  },
  getters: {
    // 计算属性
  },
  modules: {
    // 模块化
  }
})

在 main.js 中引入 Store

// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

App.mpType = 'app'

const app = new Vue({
  ...App,
  store
})
app.$mount()

3. State 和 Getter

State

State 是存储应用状态的对象,类似于组件中的 data 属性。

// store/index.js
state: {
  count: 0,
  user: null,
  isLoggedIn: false
}

在组件中访问 State:

// 方法一:直接访问
this.$store.state.count

// 方法二:使用计算属性
computed: {
  count() {
    return this.$store.state.count
  }
}

// 方法三:使用 mapState 辅助函数
import { mapState } from 'vuex'

computed: {
  ...mapState(['count', 'user', 'isLoggedIn'])
}

Getter

Getter 是从 State 中派生出的计算属性,类似于组件中的 computed 属性。

// store/index.js
getters: {
  doubleCount: state => state.count * 2,
  userInfo: state => state.user,
  isAuthenticated: state => state.isLoggedIn
}

在组件中访问 Getter:

// 方法一:直接访问
this.$store.getters.doubleCount

// 方法二:使用计算属性
computed: {
  doubleCount() {
    return this.$store.getters.doubleCount
  }
}

// 方法三:使用 mapGetters 辅助函数
import { mapGetters } from 'vuex'

computed: {
  ...mapGetters(['doubleCount', 'userInfo', 'isAuthenticated'])
}

4. Mutation 和 Action

Mutation

Mutation 是修改 State 的唯一方法,必须是同步函数。

// store/index.js
mutations: {
  increment(state) {
    state.count++
  },
  setUser(state, user) {
    state.user = user
    state.isLoggedIn = true
  },
  logout(state) {
    state.user = null
    state.isLoggedIn = false
  }
}

在组件中提交 Mutation:

// 方法一:直接提交
this.$store.commit('increment')
this.$store.commit('setUser', user)

// 方法二:使用 mapMutations 辅助函数
import { mapMutations } from 'vuex'

methods: {
  ...mapMutations(['increment', 'setUser', 'logout'])
}

Action

Action 用于处理异步操作,可以提交 Mutation。

// store/index.js
actions: {
  async login({ commit }, userInfo) {
    try {
      // 模拟 API 请求
      const response = await uni.request({
        url: 'https://api.example.com/login',
        method: 'POST',
        data: userInfo
      })
      
      const user = response.data
      commit('setUser', user)
      return user
    } catch (error) {
      console.error('Login failed:', error)
      throw error
    }
  },
  async logout({ commit }) {
    try {
      // 模拟 API 请求
      await uni.request({
        url: 'https://api.example.com/logout',
        method: 'POST'
      })
      
      commit('logout')
    } catch (error) {
      console.error('Logout failed:', error)
      throw error
    }
  }
}

在组件中分发 Action:

// 方法一:直接分发
this.$store.dispatch('login', userInfo)
  .then(user => {
    console.log('Login successful:', user)
  })
  .catch(error => {
    console.error('Login failed:', error)
  })

// 方法二:使用 mapActions 辅助函数
import { mapActions } from 'vuex'

methods: {
  ...mapActions(['login', 'logout'])
}

// 使用
this.login(userInfo)
  .then(user => {
    console.log('Login successful:', user)
  })
  .catch(error => {
    console.error('Login failed:', error)
  })

5. 模块化状态管理

当应用规模扩大时,单一的 Store 会变得臃肿和难以维护。Vuex 提供了模块化的功能,允许你将 Store 分割成多个模块。

创建模块

// store/modules/user.js
const userModule = {
  namespaced: true,
  state: {
    user: null,
    isLoggedIn: false,
    token: null
  },
  mutations: {
    setUser(state, user) {
      state.user = user
      state.isLoggedIn = true
    },
    setToken(state, token) {
      state.token = token
    },
    logout(state) {
      state.user = null
      state.isLoggedIn = false
      state.token = null
    }
  },
  actions: {
    async login({ commit }, userInfo) {
      try {
        const response = await uni.request({
          url: 'https://api.example.com/login',
          method: 'POST',
          data: userInfo
        })
        
        const { user, token } = response.data
        commit('setUser', user)
        commit('setToken', token)
        return user
      } catch (error) {
        console.error('Login failed:', error)
        throw error
      }
    },
    async logout({ commit }) {
      try {
        await uni.request({
          url: 'https://api.example.com/logout',
          method: 'POST'
        })
        
        commit('logout')
      } catch (error) {
        console.error('Logout failed:', error)
        throw error
      }
    }
  },
  getters: {
    userInfo: state => state.user,
    isAuthenticated: state => state.isLoggedIn,
    authToken: state => state.token
  }
}

export default userModule
// store/modules/counter.js
const counterModule = {
  namespaced: true,
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    },
    setCount(state, count) {
      state.count = count
    }
  },
  actions: {
    incrementAsync({ commit }) {
      return new Promise(resolve => {
        setTimeout(() => {
          commit('increment')
          resolve()
        }, 1000)
      })
    }
  },
  getters: {
    doubleCount: state => state.count * 2,
    tripleCount: state => state.count * 3
  }
}

export default counterModule

注册模块

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import counter from './modules/counter'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    user,
    counter
  }
})

使用模块化状态

// 访问模块状态
this.$store.state.user.user
this.$store.state.counter.count

// 访问模块 getter
this.$store.getters['user/userInfo']
this.$store.getters['counter/doubleCount']

// 提交模块 mutation
this.$store.commit('user/setUser', user)
this.$store.commit('counter/increment')

// 分发模块 action
this.$store.dispatch('user/login', userInfo)
this.$store.dispatch('counter/incrementAsync')

// 使用辅助函数
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

computed: {
  ...mapState('user', ['user', 'isLoggedIn']),
  ...mapState('counter', ['count']),
  ...mapGetters('user', ['userInfo', 'isAuthenticated']),
  ...mapGetters('counter', ['doubleCount'])
},
methods: {
  ...mapMutations('user', ['setUser', 'logout']),
  ...mapMutations('counter', ['increment', 'decrement']),
  ...mapActions('user', ['login', 'logout']),
  ...mapActions('counter', ['incrementAsync'])
}

实用案例分析

案例:实现全局用户状态管理

1. 创建用户状态模块

// store/modules/user.js
const userModule = {
  namespaced: true,
  state: {
    user: null,
    isLoggedIn: false,
    token: null,
    loading: false,
    error: null
  },
  mutations: {
    setLoading(state, loading) {
      state.loading = loading
    },
    setError(state, error) {
      state.error = error
    },
    setUser(state, user) {
      state.user = user
      state.isLoggedIn = true
      state.error = null
    },
    setToken(state, token) {
      state.token = token
    },
    logout(state) {
      state.user = null
      state.isLoggedIn = false
      state.token = null
      state.error = null
    }
  },
  actions: {
    async login({ commit }, userInfo) {
      try {
        commit('setLoading', true)
        commit('setError', null)
        
        // 模拟 API 请求
        const response = await uni.request({
          url: 'https://api.example.com/login',
          method: 'POST',
          data: userInfo
        })
        
        if (response.statusCode === 200) {
          const { user, token } = response.data
          commit('setUser', user)
          commit('setToken', token)
          
          // 存储 token 到本地存储
          uni.setStorageSync('token', token)
          
          return user
        } else {
          throw new Error('Login failed')
        }
      } catch (error) {
        commit('setError', error.message)
        throw error
      } finally {
        commit('setLoading', false)
      }
    },
    async logout({ commit }) {
      try {
        commit('setLoading', true)
        
        // 模拟 API 请求
        await uni.request({
          url: 'https://api.example.com/logout',
          method: 'POST'
        })
        
        // 清除本地存储的 token
        uni.removeStorageSync('token')
        commit('logout')
      } catch (error) {
        console.error('Logout failed:', error)
        // 即使 API 请求失败,也清除本地状态
        uni.removeStorageSync('token')
        commit('logout')
      } finally {
        commit('setLoading', false)
      }
    },
    async checkAuth({ commit }) {
      try {
        // 从本地存储获取 token
        const token = uni.getStorageSync('token')
        
        if (!token) {
          commit('logout')
          return false
        }
        
        commit('setLoading', true)
        
        // 验证 token
        const response = await uni.request({
          url: 'https://api.example.com/verify-token',
          method: 'POST',
          data: { token }
        })
        
        if (response.statusCode === 200) {
          const user = response.data.user
          commit('setUser', user)
          commit('setToken', token)
          return true
        } else {
          uni.removeStorageSync('token')
          commit('logout')
          return false
        }
      } catch (error) {
        console.error('Check auth failed:', error)
        uni.removeStorageSync('token')
        commit('logout')
        return false
      } finally {
        commit('setLoading', false)
      }
    }
  },
  getters: {
    userInfo: state => state.user,
    isAuthenticated: state => state.isLoggedIn,
    authToken: state => state.token,
    isLoading: state => state.loading,
    error: state => state.error
  }
}

export default userModule

2. 在应用启动时检查认证状态

// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

App.mpType = 'app'

const app = new Vue({
  ...App,
  store
})

// 应用启动时检查认证状态
store.dispatch('user/checkAuth')
  .then(isAuthenticated => {
    console.log('Auth check result:', isAuthenticated)
  })
  .catch(error => {
    console.error('Auth check failed:', error)
  })

app.$mount()

3. 登录页面示例

<!-- pages/login/login.vue -->
<template>
  <view class="container">
    <view class="login-form">
      <text class="title">登录</text>
      
      <view class="form-item">
        <input 
          type="text" 
          v-model="userInfo.username" 
          placeholder="请输入用户名"
          class="input"
        />
      </view>
      
      <view class="form-item">
        <input 
          type="password" 
          v-model="userInfo.password" 
          placeholder="请输入密码"
          class="input"
        />
      </view>
      
      <text v-if="error" class="error-message">{{ error }}</text>
      
      <button 
        @click="handleLogin" 
        :disabled="loading"
        class="login-button"
      >
        {{ loading ? '登录中...' : '登录' }}
      </button>
    </view>
  </view>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  data() {
    return {
      userInfo: {
        username: '',
        password: ''
      }
    }
  },
  computed: {
    ...mapState('user', ['loading', 'error', 'isAuthenticated'])
  },
  watch: {
    // 监听认证状态变化,登录成功后跳转到首页
    isAuthenticated(newVal) {
      if (newVal) {
        uni.switchTab({
          url: '/pages/index/index'
        })
      }
    }
  },
  methods: {
    ...mapActions('user', ['login']),
    
    handleLogin() {
      if (!this.userInfo.username || !this.userInfo.password) {
        uni.showToast({
          title: '请输入用户名和密码',
          icon: 'none'
        })
        return
      }
      
      this.login(this.userInfo)
        .catch(error => {
          console.error('Login failed:', error)
        })
    }
  }
}
</script>

<style scoped>
.container {
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 40rpx;
  background-color: #f5f5f5;
}

.login-form {
  width: 100%;
  max-width: 500rpx;
  background-color: #fff;
  padding: 60rpx;
  border-radius: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.title {
  font-size: 36rpx;
  font-weight: bold;
  text-align: center;
  margin-bottom: 60rpx;
  color: #333;
}

.form-item {
  margin-bottom: 40rpx;
}

.input {
  width: 100%;
  height: 80rpx;
  border: 2rpx solid #e0e0e0;
  border-radius: 10rpx;
  padding: 0 20rpx;
  font-size: 28rpx;
}

.error-message {
  color: #ff4d4f;
  font-size: 24rpx;
  margin-bottom: 30rpx;
  display: block;
}

.login-button {
  width: 100%;
  height: 80rpx;
  background-color: #409eff;
  color: #fff;
  font-size: 28rpx;
  font-weight: bold;
  border-radius: 10rpx;
  margin-top: 20rpx;
}

.login-button:disabled {
  background-color: #c0c4cc;
}
</style>

4. 首页示例

<!-- pages/index/index.vue -->
<template>
  <view class="container">
    <view v-if="isAuthenticated" class="user-info">
      <text class="welcome">欢迎,{{ userInfo.username }}</text>
      <button @click="handleLogout" class="logout-button">退出登录</button>
    </view>
    <view v-else class="login-prompt">
      <text>请先登录</text>
      <button @click="navigateToLogin" class="login-button">去登录</button>
    </view>
  </view>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState('user', ['isAuthenticated', 'userInfo'])
  },
  methods: {
    ...mapActions('user', ['logout']),
    
    handleLogout() {
      uni.showModal({
        title: '退出登录',
        content: '确定要退出登录吗?',
        success: (res) => {
          if (res.confirm) {
            this.logout()
              .then(() => {
                uni.switchTab({
                  url: '/pages/index/index'
                })
              })
              .catch(error => {
                console.error('Logout failed:', error)
              })
          }
        }
      })
    },
    
    navigateToLogin() {
      uni.navigateTo({
        url: '/pages/login/login'
      })
    }
  }
}
</script>

<style scoped>
.container {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 40rpx;
  background-color: #f5f5f5;
}

.user-info {
  text-align: center;
}

.welcome {
  font-size: 32rpx;
  color: #333;
  margin-bottom: 40rpx;
  display: block;
}

.logout-button {
  width: 200rpx;
  height: 60rpx;
  background-color: #ff4d4f;
  color: #fff;
  font-size: 24rpx;
  border-radius: 10rpx;
}

.login-prompt {
  text-align: center;
}

.login-prompt text {
  font-size: 32rpx;
  color: #666;
  margin-bottom: 40rpx;
  display: block;
}

.login-button {
  width: 200rpx;
  height: 60rpx;
  background-color: #409eff;
  color: #fff;
  font-size: 24rpx;
  border-radius: 10rpx;
}
</style>

常见问题与解决方案

1. 状态持久化

问题:刷新页面后,Vuex 状态会丢失。

解决方案

  • 使用 uni.setStorageSyncuni.getStorageSync 存储和读取状态
  • 在 Mutation 中同步更新本地存储
  • 在应用启动时从本地存储恢复状态
// store/modules/user.js
mutations: {
  setToken(state, token) {
    state.token = token
    // 同步更新本地存储
    uni.setStorageSync('token', token)
  },
  logout(state) {
    state.user = null
    state.isLoggedIn = false
    state.token = null
    // 清除本地存储
    uni.removeStorageSync('token')
  }
}

// 应用启动时恢复状态
const token = uni.getStorageSync('token')
if (token) {
  // 验证 token 有效性
  store.dispatch('user/checkAuth')
}

2. 模块化状态管理中的命名冲突

问题:多个模块中存在相同名称的 Mutation 或 Action 时,会发生命名冲突。

解决方案

  • 在模块中设置 namespaced: true,启用命名空间
  • 使用模块名称作为前缀访问模块的状态、Getter、Mutation 和 Action
// 启用命名空间
const userModule = {
  namespaced: true,
  // ...
}

// 使用命名空间访问
this.$store.commit('user/setUser', user)
this.$store.dispatch('user/login', userInfo)

3. 异步操作中的状态管理

问题:在异步操作中,状态更新可能不及时或不一致。

解决方案

  • 使用 Action 处理异步操作,在异步操作完成后提交 Mutation
  • 使用 Promise 或 async/await 处理异步流程
  • 添加 loading 状态,提升用户体验
// store/modules/user.js
actions: {
  async login({ commit }, userInfo) {
    try {
      commit('setLoading', true)
      // 异步操作
      const response = await uni.request({/* ... */})
      // 提交 Mutation 更新状态
      commit('setUser', response.data.user)
      commit('setToken', response.data.token)
      return response.data.user
    } catch (error) {
      commit('setError', error.message)
      throw error
    } finally {
      commit('setLoading', false)
    }
  }
}

代码优化建议

1. 使用辅助函数简化代码

// 优化前:直接访问和提交
this.$store.state.user.user
this.$store.commit('user/setUser', user)
this.$store.dispatch('user/login', userInfo)

// 优化后:使用辅助函数
import { mapState, mapMutations, mapActions } from 'vuex'

computed: {
  ...mapState('user', ['user', 'isLoggedIn'])
},
methods: {
  ...mapMutations('user', ['setUser']),
  ...mapActions('user', ['login'])
}

// 使用
this.user
this.setUser(user)
this.login(userInfo)

2. 拆分大型 Store

// 优化前:单一大型 Store
const store = new Vuex.Store({
  state: {
    // 大量状态
  },
  mutations: {
    // 大量 mutations
  },
  actions: {
    // 大量 actions
  }
})

// 优化后:模块化 Store
const store = new Vuex.Store({
  modules: {
    user: require('./modules/user').default,
    counter: require('./modules/counter').default,
    products: require('./modules/products').default
  }
})

3. 使用常量定义 Mutation 类型

// 优化前:直接使用字符串
commit('setUser', user)

// 优化后:使用常量
// store/types.js
export const SET_USER = 'SET_USER'
export const LOGOUT = 'LOGOUT'

// store/modules/user.js
import { SET_USER, LOGOUT } from '../types'

mutations: {
  [SET_USER](state, user) {
    state.user = user
    state.isLoggedIn = true
  },
  [LOGOUT](state) {
    state.user = null
    state.isLoggedIn = false
  }
}

// 使用
commit(SET_USER, user)

4. 合理使用 Getter

// 优化前:在组件中计算
computed: {
  fullName() {
    const user = this.$store.state.user.user
    return user ? `${user.firstName} ${user.lastName}` : ''
  }
}

// 优化后:在 Store 中使用 Getter
// store/modules/user.js
getters: {
  fullName: state => {
    return state.user ? `${state.user.firstName} ${state.user.lastName}` : ''
  }
}

// 组件中使用
computed: {
  ...mapGetters('user', ['fullName'])
}

章节总结

本章节详细介绍了 uni-app 中的状态管理方法,包括:

  1. Vuex 概述:介绍了 Vuex 的核心概念,包括 State、Getter、Mutation、Action 和 Module
  2. Vuex 基本使用:讲解了如何安装和配置 Vuex,以及如何在组件中使用 Vuex
  3. State 和 Getter:详细介绍了如何定义和访问 State,以及如何使用 Getter 派生计算属性
  4. Mutation 和 Action:讲解了如何使用 Mutation 修改状态,以及如何使用 Action 处理异步操作
  5. 模块化状态管理:介绍了如何将 Store 分割成多个模块,以及如何使用模块化状态
  6. 实用案例:通过实现全局用户状态管理,展示了状态管理在实际应用中的使用
  7. 常见问题与解决方案:针对状态持久化、命名冲突和异步操作中的状态管理问题提供了解决方案
  8. 代码优化建议:提供了使用辅助函数、拆分大型 Store、使用常量定义 Mutation 类型和合理使用 Getter 的优化建议

通过本章节的学习,你应该能够掌握 uni-app 中的状态管理方法,有效地管理应用状态,构建可维护性更高的应用。在实际开发中,应根据应用的规模和复杂度选择合适的状态管理方案,并遵循 Vuex 的最佳实践,确保状态管理的清晰和高效。

« 上一篇 uni-app 动画效果 下一篇 » uni-app 路由管理