状态管理 - 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,你就能构建出结构清晰、易于维护的应用!