在UniApp开发中,随着业务复杂度提升,跨页面共享用户登录状态、购物车数量、主题设置等全局数据变得至关重要。Vue 2时代我们常使用Vuex,但Vue 3官方推荐的替代品Pinia凭借其简洁的API、完整的TypeScript支持和更优的tree-shaking表现,已成为新一代状态管理的首选。UniApp对Pinia的集成非常友好,本文将手把手带你从安装到实战,构建一个可持久化的购物车状态管理系统,让数据在页面间无缝流转,并且在App重启后依然保留。
一、在UniApp项目中集成Pinia
首先,确保你的UniApp项目基于Vue 3版本(CLI或HBuilderX创建时选择Vue3)。安装Pinia非常简单,在项目根目录执行:
npm install pinia
或者使用yarn:
yarn add pinia
安装完成后,在 main.js 中注册Pinia插件:
// main.js
import App from './App'
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
return {
app,
pinia
}
}
注意UniApp使用 createSSRApp 来支持服务端渲染兼容(H5端),因此我们需要导出app和pinia。之后,你就可以在任何页面的 setup 函数中使用 useStore() 了。
二、定义第一个Store:管理用户信息
Pinia的Store分为两种模式:Options Store(类似Vuex模块)和Setup Store(利用Vue组合式API)。这里我们采用更灵活的Setup Store风格。在项目 store/ 目录下创建 user.js:
// store/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态
const token = ref('')
const userInfo = ref({
nickname: '',
avatar: ''
})
// 计算属性
const isLogin = computed(() => !!token.value)
// 动作
function login(loginData) {
// 模拟登录API请求
return new Promise((resolve) => {
setTimeout(() => {
token.value = 'sample-token-' + Date.now()
userInfo.value = {
nickname: loginData.username || '用户',
avatar: '/static/default-avatar.png'
}
resolve(true)
}, 800)
})
}
function logout() {
token.value = ''
userInfo.value = { nickname: '', avatar: '' }
}
return {
token,
userInfo,
isLogin,
login,
logout
}
})
Setup Store 直接利用 ref 和 computed 定义状态和计算属性,返回值就是暴露给外部使用的数据和方法。相比Options Store,它更加自由且易于扩展。
三、在页面中使用Store:用户登录与状态显示
创建登录页面,调用Store的 login 动作,并在个人中心页面展示用户信息。你可以在任何组件或页面中通过 useUserStore() 获取同一份状态。
登录页 pages/login/login.vue:
<template>
<view class="page">
<input v-model="username" placeholder="请输入用户名" />
<button @click="doLogin">登录</button>
<text v-if="userStore.isLogin">已登录:{{ userStore.userInfo.nickname }}</text>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { useUserStore } from '@/store/user.js';
const userStore = useUserStore();
const username = ref('');
function doLogin() {
userStore.login({ username: username.value }).then(() => {
uni.showToast({ title: '登录成功' });
uni.switchTab({ url: '/pages/mine/mine' });
});
}
</script>
个人中心页 pages/mine/mine.vue:
<template>
<view class="page">
<image :src="userStore.userInfo.avatar" />
<text>{{ userStore.userInfo.nickname }}</text>
<button v-if="userStore.isLogin" @click="userStore.logout()">退出登录</button>
<button v-else @click="goLogin">去登录</button>
</view>
</template>
<script setup>
import { useUserStore } from '@/store/user.js';
const userStore = useUserStore();
function goLogin() {
uni.navigateTo({ url: '/pages/login/login' });
}
</script>
你会发现,登录成功后切换tab到个人中心,用户信息自动呈现。这一切都归功于Pinia的响应式机制——任何引用同一store的地方都会同步更新。不需要手动刷新或传递参数。
四、复杂场景:购物车状态管理
购物车是典型的全局状态场景:商品详情页可以添加商品,购物车页面展示列表并允许修改数量或移除,同时首页或底部导航栏上要实时显示购物车总数量。我们创建一个 cart.js store:
// store/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
const items = ref([]) // { id, name, price, quantity, image }
// 计算总价
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0).toFixed(2)
})
// 计算总数量
const totalQuantity = computed(() => {
return items.value.reduce((cnt, item) => cnt + item.quantity, 0)
})
// 添加商品
function addItem(product) {
const exist = items.value.find(i => i.id === product.id)
if (exist) {
exist.quantity++
} else {
items.value.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity: 1
})
}
// 每次修改后触发持久化(稍后实现)
}
// 更新数量
function updateQuantity(productId, quantity) {
const item = items.value.find(i => i.id === productId)
if (item) {
item.quantity = quantity > 0 ? quantity : 1
}
}
// 移除商品
function removeItem(productId) {
const index = items.value.findIndex(i => i.id === productId)
if (index !== -1) {
items.value.splice(index, 1)
}
}
// 清空购物车
function clearCart() {
items.value = []
}
return {
items,
totalPrice,
totalQuantity,
addItem,
updateQuantity,
removeItem,
clearCart
}
})
在商品详情页加入购物车:
// pages/goods/detail.vue
<template>
<view>
<text>{{ product.name }}</text>
<button @click="addToCart">加入购物车</button>
</view>
</template>
<script setup>
import { reactive } from 'vue';
import { useCartStore } from '@/store/cart.js';
const cartStore = useCartStore();
const product = reactive({
id: 1001,
name: '无线蓝牙耳机',
price: 199.00,
image: '/static/headphone.png'
});
function addToCart() {
cartStore.addItem(product);
uni.showToast({ title: '已加入购物车' });
}
</script>
在底部导航栏或自定义tabbar上显示购物车数量:
<template>
<view class="tabbar">
<text>首页</text>
<text>购物车({{ cartStore.totalQuantity }})</text>
<text>我的</text>
</view>
</template>
<script setup>
import { useCartStore } from '@/store/cart.js';
const cartStore = useCartStore();
</script>
购物车数量的变化会即时反映在任何引用该store的UI组件上,完全响应式。
五、数据持久化:让状态在App重启后依然存在
目前的状态仅保存在内存中,当用户关闭小程序或退出App后就会丢失。我们需要将Pinia的状态自动同步到 uni.storage 中,并在应用启动时重新加载。这可以通过Pinia的插件机制优雅实现。
在 store/ 目录下创建 persist.js 插件:
// store/plugins/persist.js
export function persistPlugin(context) {
const { store } = context;
// 应用启动时从storage恢复状态
const key = `pinia-${store.$id}`
const saved = uni.getStorageSync(key)
if (saved) {
store.$patch(saved)
}
// 每次状态变化后保存到storage(可添加防抖优化)
store.$subscribe((mutation, state) => {
uni.setStorageSync(key, JSON.parse(JSON.stringify(state)))
})
}
然后在 main.js 中启用这个插件:
import { createPinia } from 'pinia'
import { persistPlugin } from './store/plugins/persist.js'
const pinia = createPinia()
pinia.use(persistPlugin)
这样,无论是用户信息还是购物车列表,都会自动同步到本地存储中。App冷启动后,状态依然保留。需要注意的是,uni.setStorageSync 有大小限制(一般为10MB),复杂对象需要序列化,我们通过 JSON.parse(JSON.stringify(state)) 确保只存储可序列化的数据。
对于需要更精细持久化控制的场景(比如只持久化某些字段),你也可以使用社区插件 pinia-plugin-persistedstate,它同样兼容UniApp,只需将 storage 选项指向 uni 的存储对象即可。
六、Pinia在UniApp中的注意事项
- store实例共享:在UniApp中,每个页面是独立的,但Pinia store在应用级别是单例的,所以可以放心跨页面共享。
- 条件编译:如果某些平台有特殊需求,可以在store内使用条件编译,例如
// #ifdef MP-WEIXIN。 - 组合式API与选项式API:Pinia完全兼容选项式写法,你可以在
methods中通过this.cartStore = useCartStore()访问(需在setup()中返回)。 - TypeScript支持:Pinia天生对TypeScript友好,无需额外类型声明即可获得完整的类型推断。
七、完整购物车页面演示
最后,我们给出一个购物车页面的完整代码,展示如何结合Pinia实现列表渲染、数量加减和总价计算。
<template>
<view class="cart-page">
<view v-if="cartStore.items.length === 0">购物车为空</view>
<view v-else>
<view v-for="item in cartStore.items" :key="item.id">
<image :src="item.image" />
<text>{{ item.name }}</text>
<text>¥{{ item.price }}</text>
<view>
<button @click="cartStore.updateQuantity(item.id, item.quantity - 1)">-</button>
<text>{{ item.quantity }}</text>
<button @click="cartStore.updateQuantity(item.id, item.quantity + 1)">+</button>
</view>
<button @click="cartStore.removeItem(item.id)">删除</button>
</view>
<view>合计:¥{{ cartStore.totalPrice }}</view>
<button @click="cartStore.clearCart()">清空购物车</button>
</view>
</view>
</template>
<script setup>
import { useCartStore } from '@/store/cart.js';
const cartStore = useCartStore();
</script>
至此,一个完全响应式、跨页面且可持久化的购物车模块构建完毕。任何页面对购物车的修改,都会立即反映到相关页面上,用户体验流畅且代码维护成本极低。
八、总结
通过本文的实战,你已经掌握在UniApp中集成Pinia、定义Setup Store、实现全局状态共享以及利用插件进行数据持久化的全套技能。Pinia的轻量和优雅,让状态管理不再是负担,而是提升开发效率的利器。无论是用户登录、购物车,还是主题切换、消息未读数等场景,你都可以用同样的模式快速实现。立即在你的UniApp项目中引入Pinia,感受跨页面数据同步带来的便捷吧。

