uni-app Pinia 状态管理实战:跨页面购物车案例完全指南

2026-06-08 0 152

uni-app开发中,随着业务复杂度增加,跨页面、跨组件共享数据的需求日益迫切。传统的uni.setStorageSyncVue.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属性需要传入一个实现了getItemsetItem的对象。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改写为组合式语法(使用refcomputed等):

                
// 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在大型应用中的架构价值。

uni-app Pinia 状态管理实战:跨页面购物车案例完全指南
收藏 (0) 打赏

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

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

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

淘吗网 uniapp uni-app Pinia 状态管理实战:跨页面购物车案例完全指南 https://www.taomawang.com/web/uniapp/2111.html

常见问题

相关文章

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

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