在uni-app开发中,随着业务复杂度增加,跨页面、跨组件共享数据的需求日益迫切。传统的uni.setStorageSync和Vue.prototype方式难以应对复杂的状态变化和响应式需求。而Pinia作为Vue3官方推荐的状态管理库,天然支持uni-app环境,结合组合式API(Composition API),可以构建出清晰、可维护的全局状态体系。本文将以一个完整的商品购物车功能为例,从零开始实现商品列表、购物车增减、跨页面同步和本地持久化,让你彻底掌握uni-app下的状态管理方案。
为什么在uni-app中选择Pinia?
Vue原本的Vuex虽然功能完善,但在Vue3时代显得较为重量级,且类型支持不理想。Pinia由Vuex核心成员开发,被指定为下一代官方状态管理。它在uni-app中的优势包括:
- 极简API:移除mutations,只有state、getters和actions。
- 优秀的TypeScript支持:无需额外类型声明即可获得完善的类型推导。
- 模块化设计:可以创建多个store,彼此独立又可互相引用。
- 轻量级:打包体积约1KB,对小程序包体积友好。
- 持久化轻松:社区有成熟的持久化插件,可一键实现本地存储恢复。
本次实战的目标是构建一个电商小程序的购物车功能,涉及商品列表页(增加商品)、商品详情页(可选)、购物车页面(展示与管理)以及底部导航栏上的购物车徽标。所有状态通过Pinia store统一管理,修改后自动同步到所有相关页面。
环境准备与Pinia集成
我们使用HBuilder X创建默认的Vue3模板项目。然后通过终端或在HBuilder的内置终端中安装Pinia:
npm install pinia
如果希望状态在关闭应用后依然保留,可以安装持久化插件(后续章节会用到):
npm install pinia-plugin-persist
接着在main.js中注册Pinia,并将store实例挂载到Vue应用上:
// main.js
import App from './App.vue'
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}
注意uni-app的Vue3模式需要导出createApp函数,这与普通Vue项目略有不同。完成后,我们就可以在项目中任意位置导入defineStore开始定义状态模块了。
模块化Store设计:商品与购物车
合理的store划分有助于项目长期维护。本案例中,我们创建两个store:
- useProductStore:管理商品列表数据,便于多个页面共享。
- useCartStore:管理购物车商品集合、数量、总额等。
首先,在项目根目录创建store文件夹,新建product.js:
// store/product.js
import { defineStore } from 'pinia'
export const useProductStore = defineStore('product', {
state: () => ({
list: [],
loaded: false
}),
getters: {
// 筛选热销商品
hotList: (state) => state.list.filter(item => item.hot)
},
actions: {
async fetchAll() {
// 模拟网络请求
this.list = [
{ id: 1, name: 'uni-app实战教程', price: 29.9, hot: true, image: '/static/p1.png' },
{ id: 2, name: 'Vue3组合式API精讲', price: 39.9, hot: false, image: '/static/p2.png' },
{ id: 3, name: '跨平台小程序开发', price: 49.9, hot: true, image: '/static/p3.png' },
]
this.loaded = true
}
}
})
然后是核心的cart.js:
// store/cart.js
import { defineStore } from 'pinia'
import { useProductStore } from './product'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] // [{ productId, name, price, quantity, image }]
}),
getters: {
// 总商品数量(展示在tabBar徽标)
totalQuantity: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
// 总金额
totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0).toFixed(2),
// 购物车是否为空
isEmpty: (state) => state.items.length === 0
},
actions: {
// 添加商品到购物车
addProduct(productId) {
const productStore = useProductStore()
const product = productStore.list.find(p => p.id === productId)
if (!product) return
const existIndex = this.items.findIndex(item => item.productId === productId)
if (existIndex > -1) {
this.items[existIndex].quantity += 1
} else {
this.items.push({
productId: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity: 1
})
}
},
// 增加数量
increaseQuantity(productId) {
const item = this.items.find(i => i.productId === productId)
if (item) item.quantity += 1
},
// 减少数量
decreaseQuantity(productId) {
const item = this.items.find(i => i.productId === productId)
if (item && item.quantity > 1) {
item.quantity -= 1
} else {
// 数量减到0时移除
this.removeProduct(productId)
}
},
// 删除商品
removeProduct(productId) {
this.items = this.items.filter(i => i.productId !== productId)
},
// 清空购物车
clearCart() {
this.items = []
}
}
})
注意addProduct方法中我们导入了useProductStore来获取商品原始信息。Pinia允许store之间相互引用,但务必在action内部调用,避免在顶层引入导致循环依赖。
购物车功能实现:添加、修改、删除
接下来在商品列表页使用这些store。假设我们有一个pages/product/list.vue页面:
<template>
<view class="product-list">
<view v-for="product in productStore.list" :key="product.id" class="product-item">
<image :src="product.image" mode="aspectFill"></image>
<view class="info">
<text class="name">{{ product.name }}</text>
<text class="price">¥{{ product.price }}</text>
</view>
<button @click="addToCart(product.id)">加入购物车</button>
</view>
</view>
</template>
<script setup>
import { onMounted } from 'vue'
import { useProductStore } from '@/store/product'
import { useCartStore } from '@/store/cart'
const productStore = useProductStore()
const cartStore = useCartStore()
onMounted(async () => {
if (!productStore.loaded) {
await productStore.fetchAll()
}
})
const addToCart = (productId) => {
cartStore.addProduct(productId)
uni.showToast({ title: '已加入购物车', icon: 'success' })
}
</script>
这里使用了<script setup>语法糖,代码更加简洁。任何对cartStore.items的修改都会自动触发页面更新。
跨页面通信:商品列表与购物车页面联动
创建购物车页面pages/cart/cart.vue,展示已选商品,并允许修改数量或移除:
<template>
<view class="cart-page">
<view v-if="cartStore.isEmpty" class="empty">
<text>购物车空空如也</text>
</view>
<view v-else>
<view v-for="item in cartStore.items" :key="item.productId" class="cart-item">
<image :src="item.image"></image>
<view class="detail">
<text>{{ item.name }}</text>
<text class="price">¥{{ (item.price * item.quantity).toFixed(2) }}</text>
</view>
<view class="quantity-ctrl">
<button @click="cartStore.decreaseQuantity(item.productId)">-</button>
<text>{{ item.quantity }}</text>
<button @click="cartStore.increaseQuantity(item.productId)">+</button>
</view>
<button @click="cartStore.removeProduct(item.productId)">删除</button>
</view>
<view class="summary">
<text>总计:¥{{ cartStore.totalPrice }}</text>
<button @click="checkout">结算</button>
</view>
</view>
</view>
</template>
<script setup>
import { useCartStore } from '@/store/cart'
const cartStore = useCartStore()
const checkout = () => {
if (cartStore.isEmpty) return
uni.showModal({
title: '确认订单',
content: `总计:¥${cartStore.totalPrice},是否下单?`,
success: (res) => {
if (res.confirm) {
cartStore.clearCart()
uni.showToast({ title: '下单成功' })
}
}
})
}
</script>
现在用户在商品列表页点击“加入购物车”,切换到购物车页面即可看到新增的商品,修改数量后总金额实时变化,切换回列表页再次添加商品,购物车数据保持一致——这一切都得益于Pinia store的全局响应式特性,无需手动传递参数或监听事件。
状态持久化:使用pinia-plugin-persist插件
默认情况下,Pinia的状态存储在内存中,小程序关闭或浏览器刷新后会重置。为了保留用户的购物车选择,我们需要将状态持久化到本地存储。安装pinia-plugin-persist后,在main.js中配置插件:
// main.js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
pinia.use(piniaPersist) // 启用持久化
app.use(pinia)
return { app, pinia }
}
然后在需要持久化的store中开启persist选项。修改store/cart.js:
// store/cart.js (增加 persist 配置)
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
getters: { /* ... */ },
actions: { /* ... */ },
persist: {
enabled: true,
strategies: [
{
key: 'shopping-cart', // 存储的键名
storage: uni.getStorageSync, // uni-app 环境使用 uni 的存储方法
paths: ['items'], // 只持久化 items,不存储其他临时状态
}
]
}
})
注意storage属性需要传入一个实现了getItem和setItem的对象。uni-app环境下可以直接使用uni的同步存储方法。如果未传入,插件默认使用localStorage,这在H5端可用,但小程序端会报错。因此建议手动适配:
// 创建一个适配对象
const uniStorage = {
getItem(key) {
return uni.getStorageSync(key)
},
setItem(key, value) {
uni.setStorageSync(key, value)
}
}
// 在 strategies 中使用
storage: uniStorage
这样,用户的购物车数据就会自动保存到本地缓存。再次打开应用时,插件会在初始化时从缓存恢复数据,用户看到的是之前留下的商品,体验完整闭环。
改造为组合式写法与最佳实践
Pinia同样支持通过setup函数方式定义store(即组合式API风格),这对于习惯Vue3 Composition API的开发者更加直观。下面将cart.js改写为组合式语法(使用ref、computed等):
// store/cart-composition.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { useProductStore } from './product'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const totalQuantity = computed(() => items.value.reduce((sum, i) => sum + i.quantity, 0))
const totalPrice = computed(() => items.value.reduce((sum, i) => sum + i.price * i.quantity, 0).toFixed(2))
const isEmpty = computed(() => items.value.length === 0)
function addProduct(productId) {
const productStore = useProductStore()
const product = productStore.list.find(p => p.id === productId)
if (!product) return
const exist = items.value.find(item => item.productId === productId)
if (exist) {
exist.quantity += 1
} else {
items.value.push({
productId: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity: 1
})
}
}
function increaseQuantity(productId) {
const item = items.value.find(i => i.productId === productId)
if (item) item.quantity += 1
}
function decreaseQuantity(productId) {
const item = items.value.find(i => i.productId === productId)
if (item && item.quantity > 1) {
item.quantity -= 1
} else {
removeProduct(productId)
}
}
function removeProduct(productId) {
items.value = items.value.filter(i => i.productId !== productId)
}
function clearCart() {
items.value = []
}
return { items, totalQuantity, totalPrice, isEmpty, addProduct, increaseQuantity, decreaseQuantity, removeProduct, clearCart }
})
这两种风格功能完全一致,选择哪种取决于团队偏好。组合式写法更容易与Vue3的composables模式结合,复用逻辑片段。
关于最佳实践,有几点值得强调:
- store 职责单一:不要将UI状态(如loading、提交中)与业务数据混在同一store,可以单独创建
useAppStore存放全局UI状态。 - 避免在组件中直接修改state:始终通过action修改状态,这样可以在action中加入校验、日志等处理。
- 利用getters减少组件计算:例如
totalPrice这种频繁使用的计算,放在store getters中确保全局一致。 - 小程序包体积敏感时按需使用持久化:并非所有store都需要持久化,仅为购物车、用户token等关键数据开启。
总结
本文通过一个完整的跨页面购物车案例,演示了在uni-app中集成Pinia状态管理的全过程。从项目初始化、Store模块化设计到跨页面联动和状态持久化,我们构建了一个响应式、可维护的全局数据流。Pinia凭借其简洁的API和出色的Vue3兼容性,已成为uni-app状态管理的首选方案。掌握它,你将能更自信地应对复杂的跨页面数据共享场景,编写的代码也会更加优雅和易于测试。
现在,你可以基于这个购物车demo进行扩展实践——比如加入会员折扣计算、优惠券逻辑,或是将商品收藏功能也抽象为独立的store,进一步体会Pinia在大型应用中的架构价值。

