uni-app Pinia状态管理落地:构建可跨端复用的用户会话与购物车模块

2026-06-22 0 442

接手这个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.jsmain.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.vueonLaunch 生命周期中调用 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后,项目里那些莫名其妙的 commitdispatch 不见了,取而代之的是直接可追踪的响应式数据操作。在uni-app多端场景中,配合 uni.setStorageSync 或者插件化的持久化,能很轻量地实现状态闭环。

如果你现在手里正好有uni-app项目在维护,可以先把用户模块和购物车模块重构一下,这俩模块的复用频率最高,改完之后页面逻辑会明显缩水。Pinia本身学习成本很低,花一个下午就能把所有核心概念摸熟,后面的收益却很大。

uni-app Pinia状态管理落地:构建可跨端复用的用户会话与购物车模块
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uni-app Pinia状态管理落地:构建可跨端复用的用户会话与购物车模块 https://www.taomawang.com/web/uniapp/2265.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务