接手这个uni-app项目时,前一个团队用的是Vuex。写到后面,模块嵌套了一堆,只要重构一点类型定义,整个项目就飘红一片。正好官方也在推Pinia,花了两天把全局状态重写了一遍,最直接的感受是:在跨端场景下,Pinia对TypeScript的支持和简洁的API,能把原本分散在各个页面的逻辑收拢得干干净净。
这篇文章把我在实际业务里封装的“用户会话”和“购物车”两个核心Store完整拆解出来,附上跨端缓存处理、路由拦截和多端差异踩坑记录。只要你的项目也是uni-app + Vue3,直接复制代码就能跑。
一、为什么是Pinia而不是Vuex
在uni-app里选型状态库,除了基础功能,还要考虑打包体积和跨文件代码提示。Pinia在这几方面都占优:
- 没有多余的Mutation层:原本Vuex必须通过commit调用mutation,Pinia里直接调用action或者修改state即可,语法糖更少。
- TypeScript类型推导原生支持:不需要写额外的类型声明文件,IDE补全很迅速。
- 支持Composition API风格:可以像写组合式函数一样定义Store,逻辑复用非常顺手。
- 体积更小:压缩后只有3KB左右,对uni-app的多端包体积很友好。
在uni-app中引入Pinia也很简单,先安装依赖:
npm install pinia
然后在项目入口文件(通常是 main.js 或 main.ts)中注册:
import { createSSRApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
export function createApp() {
const app = createSSRApp(App);
const pinia = createPinia();
app.use(pinia);
return { app, pinia };
}
注意uni-app的Vue3模式下通常使用 createSSRApp,这里原样套用即可。
二、案例一:构建健壮的用户会话Store
用户状态几乎是所有应用的基石。除了存储token和基本信息,还需要处理登录过期、退出清理、以及在不同平台上持久化存储的差异。
2.1 定义Store结构
// store/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
// 基础状态
const token = ref('');
const userInfo = ref(null);
const loginTime = ref(0);
// 计算属性:是否已登录
const isLogin = computed(() => {
return !!token.value && !!userInfo.value;
});
// 计算属性:剩余有效时间(示例为7天有效期)
const isTokenExpired = computed(() => {
if (!loginTime.value) return true;
const now = Date.now();
const sevenDays = 7 * 24 * 60 * 60 * 1000;
return now - loginTime.value > sevenDays;
});
// 登录操作
function doLogin(newToken, newUserInfo) {
token.value = newToken;
userInfo.value = newUserInfo;
loginTime.value = Date.now();
// 持久化存储(uni-app跨端方案)
uni.setStorageSync('user_token', newToken);
uni.setStorageSync('user_info', newUserInfo);
uni.setStorageSync('user_login_time', loginTime.value);
}
// 退出登录
function doLogout() {
token.value = '';
userInfo.value = null;
loginTime.value = 0;
// 移除本地存储
uni.removeStorageSync('user_token');
uni.removeStorageSync('user_info');
uni.removeStorageSync('user_login_time');
}
// 初始化时从本地存储恢复状态
function initUserState() {
const savedToken = uni.getStorageSync('user_token');
const savedInfo = uni.getStorageSync('user_info');
const savedTime = uni.getStorageSync('user_login_time');
if (savedToken && savedInfo && savedTime) {
token.value = savedToken;
userInfo.value = savedInfo;
loginTime.value = savedTime;
}
}
return {
token,
userInfo,
loginTime,
isLogin,
isTokenExpired,
doLogin,
doLogout,
initUserState,
};
});
2.2 在App启动时恢复状态
在 App.vue 的 onLaunch 生命周期中调用 initUserState,确保每次启动应用都能自动恢复登录状态:
import { useUserStore } from '@/store/user';
export default {
onLaunch() {
const userStore = useUserStore();
userStore.initUserState();
// 如果token已过期,直接清理
if (userStore.isTokenExpired) {
userStore.doLogout();
}
}
};
2.3 路由拦截守卫
uni-app不支持传统的Vue Router守卫,但我们可以利用 uni.addInterceptor 拦截页面跳转:
// 在main.js中配置
uni.addInterceptor('navigateTo', {
invoke(args) {
const userStore = useUserStore();
// 假设需要登录才能访问的页面路径前缀为 /pages/user/
if (args.url.startsWith('/pages/user/') && !userStore.isLogin) {
uni.showToast({ title: '请先登录', icon: 'none' });
uni.navigateTo({ url: '/pages/login/login' });
return false; // 阻止跳转
}
},
});
这种方式相比Vue Router的 beforeEach 稍微“土”一点,但在uni-app的架构下工作得很稳定。
三、案例二:跨端高性能购物车Store
购物车是另一个重逻辑模块,通常涉及频繁的增删改操作和价格计算。用Pinia的 computed 做价格汇总,可以避免在视图层写大量业务逻辑。
3.1 定义Store结构
// store/cart.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCartStore = defineStore('cart', () => {
// 购物车列表,每一项包含 id, name, price, count, img 等
const cartList = ref([]);
// 计算总价
const totalPrice = computed(() => {
return cartList.value.reduce((sum, item) => {
return sum + (item.price * item.count);
}, 0).toFixed(2);
});
// 计算总数量
const totalCount = computed(() => {
return cartList.value.reduce((sum, item) => sum + item.count, 0);
});
// 添加商品
function addToCart(product) {
const existIndex = cartList.value.findIndex(item => item.id === product.id);
if (existIndex > -1) {
// 已存在则增加数量
cartList.value[existIndex].count++;
} else {
// 不存在则新增
cartList.value.push({ ...product, count: 1 });
}
syncToStorage();
}
// 减少商品数量
function decreaseCount(productId) {
const index = cartList.value.findIndex(item => item.id === productId);
if (index > -1) {
if (cartList.value[index].count > 1) {
cartList.value[index].count--;
} else {
// 数量为1时直接移除
cartList.value.splice(index, 1);
}
}
syncToStorage();
}
// 移除商品
function removeFromCart(productId) {
const index = cartList.value.findIndex(item => item.id === productId);
if (index > -1) {
cartList.value.splice(index, 1);
}
syncToStorage();
}
// 清空购物车
function clearCart() {
cartList.value = [];
syncToStorage();
}
// 跨端同步到本地存储
function syncToStorage() {
uni.setStorageSync('cart_list', JSON.stringify(cartList.value));
}
// 初始化恢复购物车
function initCartState() {
const saved = uni.getStorageSync('cart_list');
if (saved) {
try {
cartList.value = JSON.parse(saved);
} catch (e) {
cartList.value = [];
}
}
}
return {
cartList,
totalPrice,
totalCount,
addToCart,
decreaseCount,
removeFromCart,
clearCart,
initCartState,
};
});
3.2 页面中的使用体验
在购物车页面里,逻辑变得极其清爽:
<template>
<view>
<view v-for="item in cartStore.cartList" :key="item.id">
<text>{{ item.name }}</text>
<button @click="cartStore.decreaseCount(item.id)">-</button>
<text>{{ item.count }}</text>
<button @click="cartStore.addToCart(item)">+</button>
</view>
<view>总计:{{ cartStore.totalPrice }}</view>
</view>
</template>
<script setup>
import { useCartStore } from '@/store/cart';
const cartStore = useCartStore();
cartStore.initCartState(); // 进入页面时恢复
</script>
这里没有使用任何组件内状态,所有操作直接委托给Store。之所以把 initCartState 放在页面里调用而不是全局,是为了实现“按需加载”和“实时同步”——用户可能在不同页面修改购物车,进入购物车页时重新拉取最新本地缓存。
四、跨端本地存储的“坑”与处理策略
uni-app的 uni.setStorageSync 在App、H5、小程序下的行为并不完全一致,这是最容易踩坑的地方:
| 平台 | 底层实现 | 容量限制 | 特殊注意 |
|---|---|---|---|
| H5 | localStorage | 5MB左右 | 只能存字符串,存大JSON时注意序列化性能 |
| App | 原生Storage | 无严格限制 | 同步读写会阻塞UI线程,大数据建议异步 |
| 小程序 | wx.setStorageSync | 10MB上限 | 频繁读写可能触发清理警告,建议加防抖 |
我们的处理建议:
- 小数据用同步,大数据用异步:用户信息这种几十字段的用
Sync没问题,购物车有几百条商品时改用uni.setStorage避免卡顿。 - 存储前做容量预估:在H5端可以用
JSON.stringify算一下长度,超过2MB就建议做分片或者只保留最近数据。 - 异常捕获:存储操作最好用
try...catch包裹,因为用户在系统设置里清理了应用缓存或者空间不足时,Storage可能会抛错。
五、Pinia在uni-app中的插件化持久存储
上面两个Store都在内部手动调用了 uni.setStorageSync。如果项目里Store数量一多,这种写法就很重复。可以写一个简单的Pinia插件自动处理:
// plugins/pinia-persist.js
export function piniaPersistPlugin(context) {
const { store } = context;
// 只对开启了persist的Store做处理
if (!store.persist) return;
// 从本地缓存恢复
const savedData = uni.getStorageSync(`pinia_${store.$id}`);
if (savedData) {
try {
store.$patch(JSON.parse(savedData));
} catch (e) {
console.warn('Store恢复失败', e);
}
}
// 监听变化并保存
store.$subscribe((mutation, state) => {
uni.setStorageSync(`pinia_${store.$id}`, JSON.stringify(state));
});
}
在创建Pinia实例时注册插件:
const pinia = createPinia();
pinia.use(piniaPersistPlugin);
之后在Store里只需要加一个标记:
export const useCartStore = defineStore('cart', () => {
// ... 逻辑...
return { ... };
}, {
persist: true, // 开启持久化
});
这种插件的写法虽然方便,但在涉及多平台差异(如容量控制)时灵活性稍弱。我个人更推荐核心的Token用显式手动控制(保证安全),非关键的UI状态才用插件自动化。
六、总结
把全局状态从Vuex迁到Pinia后,项目里那些莫名其妙的 commit 和 dispatch 不见了,取而代之的是直接可追踪的响应式数据操作。在uni-app多端场景中,配合 uni.setStorageSync 或者插件化的持久化,能很轻量地实现状态闭环。
如果你现在手里正好有uni-app项目在维护,可以先把用户模块和购物车模块重构一下,这俩模块的复用频率最高,改完之后页面逻辑会明显缩水。Pinia本身学习成本很低,花一个下午就能把所有核心概念摸熟,后面的收益却很大。

