Skip to content

状态管理 - Vuex在UniApp中的使用

🗃️ 当应用变得复杂时,状态管理就变得至关重要,今天我们来学习Vuex

1. Vuex基础概念

安装和配置

bash
# 安装Vuex
npm install vuex@3
javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0,
    userInfo: null,
    isLoggedIn: false
  },
  
  mutations: {
    INCREMENT(state) {
      state.count++
    },
    DECREMENT(state) {
      state.count--
    },
    SET_USER_INFO(state, userInfo) {
      state.userInfo = userInfo
      state.isLoggedIn = !!userInfo
    },
    CLEAR_USER_INFO(state) {
      state.userInfo = null
      state.isLoggedIn = false
    }
  },
  
  actions: {
    increment({ commit }) {
      commit('INCREMENT')
    },
    decrement({ commit }) {
      commit('DECREMENT')
    },
    async login({ commit }, loginData) {
      try {
        // 模拟登录请求
        const response = await uni.request({
          url: '/api/login',
          method: 'POST',
          data: loginData
        })
        
        const userInfo = response.data
        commit('SET_USER_INFO', userInfo)
        
        // 保存到本地存储
        uni.setStorageSync('userInfo', userInfo)
        
        return userInfo
      } catch (error) {
        throw error
      }
    },
    logout({ commit }) {
      commit('CLEAR_USER_INFO')
      uni.removeStorageSync('userInfo')
    }
  },
  
  getters: {
    doubleCount: state => state.count * 2,
    isVip: state => state.userInfo?.vip || false,
    userName: state => state.userInfo?.name || '游客'
  }
})

export default store

在main.js中注册

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

Vue.config.productionTip = false
App.mpType = 'app'

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

2. 在组件中使用Vuex

基础使用

vue
<template>
  <view class="vuex-demo">
    <!-- 显示状态 -->
    <view class="counter-section">
      <text class="count">计数:\{\{ count \}\}</text>
      <text class="double-count">双倍:\{\{ doubleCount \}\}</text>
      
      <view class="counter-buttons">
        <button @click="increment">+1</button>
        <button @click="decrement">-1</button>
      </view>
    </view>
    
    <!-- 用户信息 -->
    <view class="user-section">
      <view v-if="isLoggedIn" class="user-info">
        <text class="welcome">欢迎,\{\{ userName \}\}</text>
        <text v-if="isVip" class="vip-badge">VIP</text>
        <button @click="handleLogout">退出登录</button>
      </view>
      <view v-else class="login-prompt">
        <text>未登录</text>
        <button @click="handleLogin">立即登录</button>
      </view>
    </view>
  </view>
</template>

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

export default {
  computed: {
    // 映射state
    ...mapState(['count', 'isLoggedIn']),
    
    // 映射getters
    ...mapGetters(['doubleCount', 'isVip', 'userName'])
  },
  
  methods: {
    // 映射actions
    ...mapActions(['increment', 'decrement', 'login', 'logout']),
    
    async handleLogin() {
      try {
        await this.login({
          username: 'demo',
          password: '123456'
        })
        
        uni.showToast({
          title: '登录成功',
          icon: 'success'
        })
      } catch (error) {
        uni.showToast({
          title: '登录失败',
          icon: 'error'
        })
      }
    },
    
    handleLogout() {
      uni.showModal({
        title: '确认退出',
        content: '确定要退出登录吗?',
        success: (res) => {
          if (res.confirm) {
            this.logout()
            uni.showToast({
              title: '已退出登录',
              icon: 'success'
            })
          }
        }
      })
    }
  }
}
</script>

<style>
.vuex-demo {
  padding: 20rpx;
}
.counter-section,
.user-section {
  background: white;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
}
.count,
.double-count {
  display: block;
  font-size: 32rpx;
  margin-bottom: 20rpx;
}
.counter-buttons {
  display: flex;
  gap: 20rpx;
}
.counter-buttons button {
  flex: 1;
}
.user-info {
  display: flex;
  align-items: center;
  gap: 20rpx;
}
.welcome {
  flex: 1;
  font-size: 28rpx;
}
.vip-badge {
  background: #ff6b6b;
  color: white;
  padding: 4rpx 12rpx;
  border-radius: 20rpx;
  font-size: 20rpx;
}
.login-prompt {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>

3. 模块化Store

用户模块

javascript
// store/modules/user.js
const user = {
  namespaced: true,
  
  state: {
    info: null,
    token: null,
    permissions: []
  },
  
  mutations: {
    SET_INFO(state, info) {
      state.info = info
    },
    SET_TOKEN(state, token) {
      state.token = token
    },
    SET_PERMISSIONS(state, permissions) {
      state.permissions = permissions
    },
    CLEAR_ALL(state) {
      state.info = null
      state.token = null
      state.permissions = []
    }
  },
  
  actions: {
    async login({ commit }, { username, password }) {
      try {
        const response = await uni.request({
          url: '/api/auth/login',
          method: 'POST',
          data: { username, password }
        })
        
        const { user, token, permissions } = response.data
        
        commit('SET_INFO', user)
        commit('SET_TOKEN', token)
        commit('SET_PERMISSIONS', permissions)
        
        // 持久化存储
        uni.setStorageSync('token', token)
        uni.setStorageSync('userInfo', user)
        
        return response.data
      } catch (error) {
        throw error
      }
    },
    
    async getUserInfo({ commit, state }) {
      if (!state.token) {
        throw new Error('未登录')
      }
      
      try {
        const response = await uni.request({
          url: '/api/user/info',
          header: {
            'Authorization': `Bearer \${state.token}`
          }
        })
        
        commit('SET_INFO', response.data)
        return response.data
      } catch (error) {
        throw error
      }
    },
    
    logout({ commit }) {
      commit('CLEAR_ALL')
      uni.removeStorageSync('token')
      uni.removeStorageSync('userInfo')
    }
  },
  
  getters: {
    isLoggedIn: state => !!state.token,
    userName: state => state.info?.name || '游客',
    avatar: state => state.info?.avatar || '/static/default-avatar.png',
    hasPermission: state => permission => state.permissions.includes(permission)
  }
}

export default user

购物车模块

javascript
// store/modules/cart.js
const cart = {
  namespaced: true,
  
  state: {
    items: []
  },
  
  mutations: {
    ADD_ITEM(state, product) {
      const existingItem = state.items.find(item => item.id === product.id)
      
      if (existingItem) {
        existingItem.quantity += product.quantity || 1
      } else {
        state.items.push({
          ...product,
          quantity: product.quantity || 1
        })
      }
    },
    
    REMOVE_ITEM(state, productId) {
      const index = state.items.findIndex(item => item.id === productId)
      if (index > -1) {
        state.items.splice(index, 1)
      }
    },
    
    UPDATE_QUANTITY(state, { productId, quantity }) {
      const item = state.items.find(item => item.id === productId)
      if (item) {
        if (quantity <= 0) {
          const index = state.items.findIndex(item => item.id === productId)
          state.items.splice(index, 1)
        } else {
          item.quantity = quantity
        }
      }
    },
    
    CLEAR_CART(state) {
      state.items = []
    },
    
    LOAD_CART(state, items) {
      state.items = items || []
    }
  },
  
  actions: {
    addToCart({ commit, state }, product) {
      commit('ADD_ITEM', product)
      
      // 持久化到本地存储
      uni.setStorageSync('cartItems', state.items)
      
      uni.showToast({
        title: '已加入购物车',
        icon: 'success'
      })
    },
    
    removeFromCart({ commit, state }, productId) {
      commit('REMOVE_ITEM', productId)
      uni.setStorageSync('cartItems', state.items)
    },
    
    updateQuantity({ commit, state }, payload) {
      commit('UPDATE_QUANTITY', payload)
      uni.setStorageSync('cartItems', state.items)
    },
    
    clearCart({ commit }) {
      commit('CLEAR_CART')
      uni.removeStorageSync('cartItems')
    },
    
    loadCart({ commit }) {
      const items = uni.getStorageSync('cartItems')
      commit('LOAD_CART', items)
    }
  },
  
  getters: {
    totalItems: state => state.items.reduce((total, item) => total + item.quantity, 0),
    totalPrice: state => state.items.reduce((total, item) => total + (item.price * item.quantity), 0),
    isEmpty: state => state.items.length === 0
  }
}

export default cart

组合模块

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

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    user,
    cart
  },
  
  // 全局状态
  state: {
    loading: false,
    networkStatus: 'unknown'
  },
  
  mutations: {
    SET_LOADING(state, loading) {
      state.loading = loading
    },
    SET_NETWORK_STATUS(state, status) {
      state.networkStatus = status
    }
  },
  
  actions: {
    // 初始化应用
    async initApp({ dispatch, commit }) {
      commit('SET_LOADING', true)
      
      try {
        // 加载购物车
        await dispatch('cart/loadCart')
        
        // 检查登录状态
        const token = uni.getStorageSync('token')
        if (token) {
          commit('user/SET_TOKEN', token)
          await dispatch('user/getUserInfo')
        }
        
        // 获取网络状态
        uni.getNetworkType({
          success: (res) => {
            commit('SET_NETWORK_STATUS', res.networkType)
          }
        })
        
      } catch (error) {
        console.error('应用初始化失败:', error)
      } finally {
        commit('SET_LOADING', false)
      }
    }
  }
})

export default store

4. 在页面中使用模块化Store

vue
<template>
  <view class="shopping-page">
    <!-- 用户信息 -->
    <view class="user-header">
      <image :src="avatar" class="user-avatar" />
      <text class="user-name">\{\{ userName \}\}</text>
      <view v-if="!isLoggedIn" class="login-btn" @click="handleLogin">
        <text>登录</text>
      </view>
    </view>
    
    <!-- 商品列表 -->
    <view class="product-list">
      <view 
        v-for="product in products" 
        :key="product.id"
        class="product-item"
      >
        <image :src="product.image" class="product-image" />
        <view class="product-info">
          <text class="product-name">\{\{ product.name \}\}</text>
          <text class="product-price">¥\{\{ product.price \}\}</text>
        </view>
        <button @click="addToCart(product)" class="add-btn">
          加入购物车
        </button>
      </view>
    </view>
    
    <!-- 购物车悬浮按钮 -->
    <view class="cart-fab" @click="goToCart">
      <text class="cart-icon">🛒</text>
      <view v-if="totalItems > 0" class="cart-badge">
        <text>\{\{ totalItems \}\}</text>
      </view>
    </view>
  </view>
</template>

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

export default {
  data() {
    return {
      products: [
        { id: 1, name: 'iPhone 15', price: 5999, image: '/static/iphone.jpg' },
        { id: 2, name: 'iPad Air', price: 4599, image: '/static/ipad.jpg' },
        { id: 3, name: 'MacBook Pro', price: 14999, image: '/static/macbook.jpg' }
      ]
    }
  },
  
  computed: {
    // 用户模块
    ...mapGetters('user', ['isLoggedIn', 'userName', 'avatar']),
    
    // 购物车模块
    ...mapGetters('cart', ['totalItems', 'totalPrice']),
    
    // 全局状态
    ...mapState(['loading'])
  },
  
  methods: {
    // 用户操作
    ...mapActions('user', ['login']),
    
    // 购物车操作
    ...mapActions('cart', ['addToCart']),
    
    async handleLogin() {
      try {
        await this.login({
          username: 'demo',
          password: '123456'
        })
        
        uni.showToast({
          title: '登录成功',
          icon: 'success'
        })
      } catch (error) {
        uni.showToast({
          title: '登录失败',
          icon: 'error'
        })
      }
    },
    
    goToCart() {
      uni.navigateTo({
        url: '/pages/cart/cart'
      })
    }
  }
}
</script>

<style>
.shopping-page {
  padding: 20rpx;
}
.user-header {
  display: flex;
  align-items: center;
  padding: 30rpx;
  background: white;
  border-radius: 16rpx;
  margin-bottom: 20rpx;
}
.user-avatar {
  width: 80rpx;
  height: 80rpx;
  border-radius: 40rpx;
  margin-right: 20rpx;
}
.user-name {
  flex: 1;
  font-size: 32rpx;
  font-weight: bold;
}
.login-btn {
  background: #007aff;
  color: white;
  padding: 16rpx 32rpx;
  border-radius: 40rpx;
}
.product-list {
  background: white;
  border-radius: 16rpx;
  padding: 20rpx;
}
.product-item {
  display: flex;
  align-items: center;
  padding: 20rpx 0;
  border-bottom: 1rpx solid #f0f0f0;
}
.product-image {
  width: 120rpx;
  height: 120rpx;
  border-radius: 12rpx;
  margin-right: 20rpx;
}
.product-info {
  flex: 1;
}
.product-name {
  font-size: 28rpx;
  font-weight: bold;
  display: block;
  margin-bottom: 10rpx;
}
.product-price {
  color: #ff6b6b;
  font-size: 32rpx;
  font-weight: bold;
}
.add-btn {
  background: #007aff;
  color: white;
  border: none;
  border-radius: 8rpx;
  padding: 16rpx 24rpx;
  font-size: 24rpx;
}
.cart-fab {
  position: fixed;
  right: 30rpx;
  bottom: 30rpx;
  width: 100rpx;
  height: 100rpx;
  background: #ff6b6b;
  border-radius: 50rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 8rpx 20rpx rgba(255, 107, 107, 0.3);
}
.cart-icon {
  font-size: 40rpx;
}
.cart-badge {
  position: absolute;
  top: -10rpx;
  right: -10rpx;
  background: #ff3b30;
  color: white;
  width: 40rpx;
  height: 40rpx;
  border-radius: 20rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20rpx;
}
</style>

5. 持久化插件

javascript
// store/plugins/persistence.js
const persistence = store => {
  // 应用启动时恢复状态
  const savedState = uni.getStorageSync('vuex-state')
  if (savedState) {
    store.replaceState({
      ...store.state,
      ...savedState
    })
  }
  
  // 监听状态变化并保存
  store.subscribe((mutation, state) => {
    // 只保存需要持久化的状态
    const persistedState = {
      user: state.user,
      cart: state.cart
    }
    
    uni.setStorageSync('vuex-state', persistedState)
  })
}

export default persistence

小结

今天我们学习了:

  • ✅ Vuex的基础概念和配置
  • ✅ 在组件中使用Vuex的各种方式
  • ✅ 模块化Store的设计和实现
  • ✅ 状态持久化的处理
  • ✅ 实际项目中的状态管理实践

状态管理要点

  • 合理设计state结构
  • 使用模块化管理复杂状态
  • 注意状态的持久化需求
  • 遵循单向数据流原则

下一篇预告

下一篇我们将学习《插件和第三方库 - 站在巨人的肩膀上》,学习如何使用和集成第三方插件。


状态管理是大型应用的基石,掌握了Vuex,你就能构建出结构清晰、易于维护的应用!