uni-app作为一款跨平台框架,一套代码可发布到iOS、Android、H5以及各种小程序。然而,随着应用规模增长,状态管理和代码组织成为挑战。本文通过构建一个模块化商城应用,完整演示如何使用Vuex进行状态管理,并结合分包策略优化性能,同时保持跨平台一致性。
一、为什么需要模块化状态管理?
在uni-app中,页面间共享数据(如用户登录态、购物车)是常见需求。如果每个页面独立管理数据,会导致数据不一致和代码冗余。Vuex提供了集中式存储,而模块化(Module)则让状态管理按业务领域拆分,例如:
user模块:用户信息、登录状态cart模块:购物车商品、数量product模块:商品列表、分类
这样每个模块独立维护自己的state、mutations、actions,避免命名冲突,也便于团队协作。
二、项目结构设计
我们采用uni-app官方推荐的目录结构,结合模块化Vuex:
┌─ components # 公共组件
│ ├─ product-card.vue
│ └─ cart-badge.vue
├─ pages # 页面
│ ├─ index # 首页
│ ├─ category # 分类页
│ ├─ cart # 购物车页
│ └─ mine # 个人中心
├─ store # Vuex状态管理
│ ├─ index.js # 主入口
│ ├─ modules
│ │ ├─ user.js # 用户模块
│ │ ├─ cart.js # 购物车模块
│ │ └─ product.js # 商品模块
│ └─ getters.js # 全局getters
├─ api # API请求封装
│ └─ index.js
├─ utils # 工具函数
├─ static # 静态资源
├─ App.vue
├─ main.js
├─ manifest.json
└─ pages.json
三、Vuex模块化实现
1. 用户模块(store/modules/user.js)
export default {
namespaced: true, // 启用命名空间
state: {
token: '',
userInfo: null,
isLogin: false
},
getters: {
// 获取用户昵称,未登录显示默认
displayName: (state) => {
return state.userInfo?.nickname || '未登录用户'
}
},
mutations: {
SET_TOKEN(state, token) {
state.token = token
state.isLogin = !!token
},
SET_USER_INFO(state, info) {
state.userInfo = info
},
LOGOUT(state) {
state.token = ''
state.userInfo = null
state.isLogin = false
// 清除本地存储
uni.removeStorageSync('token')
}
},
actions: {
// 模拟登录
async login({ commit }, { username, password }) {
// 实际项目调用api
const res = await new Promise((resolve) => {
setTimeout(() => {
resolve({
token: 'mock_token_' + Date.now(),
userInfo: { nickname: username, avatar: '' }
})
}, 500)
})
commit('SET_TOKEN', res.token)
commit('SET_USER_INFO', res.userInfo)
uni.setStorageSync('token', res.token)
return res
},
// 检查登录状态(从本地存储恢复)
async checkLogin({ commit }) {
const token = uni.getStorageSync('token')
if (token) {
commit('SET_TOKEN', token)
// 可以在这里请求用户信息
commit('SET_USER_INFO', { nickname: '已登录用户', avatar: '' })
}
}
}
}
2. 购物车模块(store/modules/cart.js)
export default {
namespaced: true,
state: {
items: [] // { id, name, price, quantity, image }
},
getters: {
totalCount: (state) => {
return state.items.reduce((sum, item) => sum + item.quantity, 0)
},
totalPrice: (state) => {
return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
},
mutations: {
ADD_ITEM(state, product) {
const existing = state.items.find(item => item.id === product.id)
if (existing) {
existing.quantity += product.quantity || 1
} else {
state.items.push({
...product,
quantity: product.quantity || 1
})
}
},
REMOVE_ITEM(state, productId) {
state.items = state.items.filter(item => item.id !== productId)
},
UPDATE_QUANTITY(state, { id, quantity }) {
const item = state.items.find(item => item.id === id)
if (item) {
item.quantity = quantity
}
},
CLEAR_CART(state) {
state.items = []
}
},
actions: {
addToCart({ commit }, product) {
commit('ADD_ITEM', product)
// 可以在这里显示提示
uni.showToast({ title: '已加入购物车', icon: 'success' })
}
}
}
3. 主入口(store/index.js)
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import cart from './modules/cart'
import product from './modules/product'
import getters from './getters'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
user,
cart,
product
},
getters
})
export default store
四、页面中使用状态管理
1. 首页商品列表(pages/index/index.vue)
<template>
<view class="container">
<view class="product-grid">
<view v-for="item in productList" :key="item.id" class="product-card" @click="goDetail(item)">
<image :src="item.image" mode="aspectFill" class="product-image"></image>
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<text class="product-price">¥{{ item.price }}</text>
<button type="primary" size="mini" @click.stop="addToCart(item)">加入购物车</button>
</view>
</view>
</view>
</view>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
computed: {
...mapState('product', ['productList'])
},
methods: {
...mapActions('cart', ['addToCart']),
goDetail(product) {
uni.navigateTo({ url: `/pages/detail/detail?id=${product.id}` })
}
},
onLoad() {
// 触发商品模块的加载动作
this.$store.dispatch('product/loadProducts')
}
}
</script>
2. 购物车页面(pages/cart/cart.vue)
<template>
<view class="cart-page">
<view v-if="items.length === 0" class="empty-cart">
<text>购物车是空的</text>
</view>
<view v-else>
<view v-for="item in items" :key="item.id" class="cart-item">
<image :src="item.image" mode="aspectFill" class="item-image"></image>
<view class="item-info">
<text class="item-name">{{ item.name }}</text>
<text class="item-price">¥{{ item.price * item.quantity }}</text>
<view class="quantity-control">
<button size="mini" @click="decrease(item)">-</button>
<text>{{ item.quantity }}</text>
<button size="mini" @click="increase(item)">+</button>
</view>
</view>
<button type="warn" size="mini" @click="remove(item.id)">删除</button>
</view>
<view class="cart-footer">
<text>合计: ¥{{ totalPrice }}</text>
<button type="primary" @click="checkout">结算</button>
</view>
</view>
</view>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
computed: {
...mapState('cart', ['items']),
...mapGetters('cart', ['totalPrice', 'totalCount'])
},
methods: {
...mapMutations('cart', ['UPDATE_QUANTITY', 'REMOVE_ITEM', 'CLEAR_CART']),
increase(item) {
this.UPDATE_QUANTITY({ id: item.id, quantity: item.quantity + 1 })
},
decrease(item) {
if (item.quantity > 1) {
this.UPDATE_QUANTITY({ id: item.id, quantity: item.quantity - 1 })
} else {
this.REMOVE_ITEM(item.id)
}
},
remove(id) {
this.REMOVE_ITEM(id)
uni.showToast({ title: '已删除', icon: 'none' })
},
checkout() {
// 跳转到结算页
uni.navigateTo({ url: '/pages/checkout/checkout' })
}
}
}
</script>
五、模块化分包策略
uni-app支持分包加载,将不同业务模块的页面放在不同分包中,减少首屏加载体积。我们在pages.json中配置:
{
"pages": [
{"path": "pages/index/index", "style": {}},
{"path": "pages/category/category", "style": {}},
{"path": "pages/cart/cart", "style": {}},
{"path": "pages/mine/mine", "style": {}}
],
"subPackages": [
{
"root": "subpackages/product",
"pages": [
{"path": "detail/detail", "style": {}},
{"path": "search/search", "style": {}}
]
},
{
"root": "subpackages/order",
"pages": [
{"path": "list/list", "style": {}},
{"path": "detail/detail", "style": {}}
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["subpackages/product"]
}
}
}
这样,商品详情和搜索页面在需要时才加载,而首页预加载了商品分包,提升用户体验。
六、跨平台适配技巧
- 条件编译:使用
#ifdef和#ifndef处理平台差异,例如微信小程序登录与App端登录逻辑不同。 - API封装:将uni-app的API(如
uni.request)封装在api/index.js中,便于统一处理错误和token。 - 自定义导航栏:使用
uni-nav-bar组件并配置titleNView,保持各平台一致性。
// api/index.js
const request = (url, data = {}, method = 'GET') => {
return new Promise((resolve, reject) => {
uni.request({
url: 'https://api.example.com' + url,
data,
method,
header: {
'Authorization': uni.getStorageSync('token') || ''
},
success: (res) => {
if (res.data.code === 0) {
resolve(res.data.data)
} else {
reject(res.data.message)
}
},
fail: reject
})
})
}
七、性能优化建议
- 合理使用getters:对于计算密集型数据(如购物车总价),使用getters缓存结果。
- 避免频繁commit:批量操作时使用
action封装多个mutation。 - 分包预加载:通过
preloadRule预加载用户可能访问的分包。 - 图片懒加载:使用
lazy-load属性或IntersectionObserver。
八、总结
通过模块化状态管理和分包策略,我们构建了一个结构清晰、性能优化的uni-app商城应用。Vuex的命名空间模块让代码易于维护,分包加载让首屏速度更快。这套架构可以轻松扩展到更大的项目,同时保持跨平台一致性。
uni-app的生态日益成熟,掌握这些高级技巧,能让你在跨平台开发中游刃有余。现在就去重构你的项目吧!
本文为原创技术教程,代码基于uni-app 3.x和Vuex 3.x。建议在实际项目中结合HBuilderX进行调试。

