uni-app组合式API与状态管理实战:构建跨平台电商应用

2026-05-19 0 431

在现代跨平台开发中,uni-app结合Vue3组合式APIPinia状态管理,为开发者提供了高效、可维护的解决方案。本文通过构建一个电商应用,完整演示如何使用这些技术实现商品展示、购物车管理和用户交互。

一、为什么需要组合式API与状态管理?

传统选项式API在复杂组件中容易出现逻辑分散、难以复用的问题。组合式API(Composition API)允许我们按功能组织代码,而Pinia作为Vue3官方推荐的状态管理库,提供了类型安全、轻量级的全局状态管理。

  • 组合式API:逻辑复用更灵活,代码组织更清晰
  • Pinia:模块化状态管理,支持TypeScript
  • uni-app:一套代码发布到iOS、Android、H5及小程序

二、项目目标:构建电商应用

我们将实现一个电商应用的核心功能:商品列表、商品详情、购物车管理和用户登录。要求:

  • 使用组合式API封装业务逻辑
  • 使用Pinia管理购物车和用户状态
  • 支持跨平台适配(H5和微信小程序)
  • 展示自定义hooks和组件通信

三、完整代码实现

1. 项目初始化

# 创建uni-app项目(使用Vue3/Vite模板)
npx degit dcloudio/uni-preset-vue#vite-ts uni-app-shop
cd uni-app-shop
npm install
npm install pinia @dcloudio/uni-ui
    

2. Pinia Store定义

// stores/cart.ts - 购物车状态管理
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // 状态
  const items = ref([])
  
  // 计算属性
  const totalCount = computed(() => 
    items.value.reduce((sum, item) => sum + item.count, 0)
  )
  
  const totalPrice = computed(() => 
    items.value.reduce((sum, item) => sum + item.price * item.count, 0)
  )
  
  // 操作
  function addItem(product: Product, count = 1) {
    const existing = items.value.find(item => item.id === product.id)
    if (existing) {
      existing.count += count
    } else {
      items.value.push({ ...product, count })
    }
    // 持久化到本地存储
    uni.setStorageSync('cart', items.value)
  }
  
  function removeItem(id: number) {
    items.value = items.value.filter(item => item.id !== id)
    uni.setStorageSync('cart', items.value)
  }
  
  function clearCart() {
    items.value = []
    uni.setStorageSync('cart', [])
  }
  
  // 初始化时从本地存储加载
  const savedCart = uni.getStorageSync('cart')
  if (savedCart) {
    items.value = savedCart
  }
  
  return { items, totalCount, totalPrice, addItem, removeItem, clearCart }
})

// stores/user.ts - 用户状态管理
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('user', () => {
  const userInfo = ref(null)
  const token = ref('')
  
  function login(credentials: { username: string; password: string }) {
    // 模拟登录请求
    return new Promise((resolve) => {
      setTimeout(() => {
        userInfo.value = { id: 1, nickname: '张三', avatar: '' }
        token.value = 'mock-token-' + Date.now()
        uni.setStorageSync('token', token.value)
        resolve()
      }, 1000)
    })
  }
  
  function logout() {
    userInfo.value = null
    token.value = ''
    uni.removeStorageSync('token')
  }
  
  return { userInfo, token, login, logout }
})

// types/index.ts - 类型定义
export interface Product {
  id: number
  name: string
  price: number
  image: string
  description: string
}

export interface CartItem extends Product {
  count: number
}

export interface UserInfo {
  id: number
  nickname: string
  avatar: string
}
    

3. 自定义Hooks(组合式API)

// hooks/useProductList.ts - 商品列表逻辑
import { ref, onMounted } from 'vue'
import type { Product } from '@/types'

export function useProductList() {
  const products = ref([])
  const loading = ref(false)
  const error = ref('')
  
  async function fetchProducts() {
    loading.value = true
    error.value = ''
    try {
      // 模拟API请求
      await new Promise(resolve => setTimeout(resolve, 500))
      products.value = [
        { id: 1, name: 'Vue3实战指南', price: 59.9, image: '/static/book1.png', description: '深入学习Vue3组合式API' },
        { id: 2, name: 'uni-app跨平台开发', price: 69.9, image: '/static/book2.png', description: '一套代码多端运行' },
        { id: 3, name: 'Pinia状态管理', price: 39.9, image: '/static/book3.png', description: '轻量级状态管理库' },
        { id: 4, name: 'TypeScript入门', price: 49.9, image: '/static/book4.png', description: '掌握TypeScript基础' },
      ]
    } catch (e) {
      error.value = '加载失败,请重试'
    } finally {
      loading.value = false
    }
  }
  
  onMounted(() => {
    fetchProducts()
  })
  
  return { products, loading, error, refresh: fetchProducts }
}

// hooks/useCartOperation.ts - 购物车操作逻辑
import { useCartStore } from '@/stores/cart'
import type { Product } from '@/types'

export function useCartOperation() {
  const cartStore = useCartStore()
  
  function addToCart(product: Product) {
    cartStore.addItem(product)
    uni.showToast({ title: '已加入购物车', icon: 'success' })
  }
  
  function buyNow(product: Product) {
    cartStore.addItem(product)
    uni.switchTab({ url: '/pages/cart/index' })
  }
  
  return { addToCart, buyNow, cartStore }
}
    

4. 商品列表页面

<!-- pages/index/index.vue -->
<template>
  <view class="container">
    <uni-nav-bar title="商品列表" fixed></uni-nav-bar>
    
    <view v-if="loading" class="loading">
      <uni-load-more status="loading"></uni-load-more>
    </view>
    
    <view v-else-if="error" class="error">
      <text>{{ error }}</text>
      <button @click="refresh">重新加载</button>
    </view>
    
    <view v-else class="product-list">
      <view 
        v-for="product in products" 
        :key="product.id" 
        class="product-item"
        @click="goDetail(product.id)"
      >
        <image :src="product.image" class="product-image"></image>
        <view class="product-info">
          <text class="product-name">{{ product.name }}</text>
          <text class="product-desc">{{ product.description }}</text>
          <text class="product-price">¥{{ product.price }}</text>
          <view class="actions">
            <button size="mini" type="primary" @click.stop="addToCart(product)">加入购物车</button>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { useProductList } from '@/hooks/useProductList'
import { useCartOperation } from '@/hooks/useCartOperation'
import type { Product } from '@/types'

const { products, loading, error, refresh } = useProductList()
const { addToCart } = useCartOperation()

function goDetail(id: number) {
  uni.navigateTo({ url: `/pages/detail/index?id=${id}` })
}
</script>
    

5. 购物车页面

<!-- pages/cart/index.vue -->
<template>
  <view class="container">
    <uni-nav-bar title="购物车" fixed></uni-nav-bar>
    
    <view v-if="cartStore.items.length === 0" class="empty">
      <text>购物车是空的</text>
      <button @click="goHome">去逛逛</button>
    </view>
    
    <view v-else class="cart-list">
      <view v-for="item in cartStore.items" :key="item.id" class="cart-item">
        <image :src="item.image" class="item-image"></image>
        <view class="item-info">
          <text class="item-name">{{ item.name }}</text>
          <text class="item-price">¥{{ item.price }}</text>
          <view class="quantity">
            <button size="mini" @click="decrease(item.id)">-</button>
            <text>{{ item.count }}</text>
            <button size="mini" @click="increase(item.id)">+</button>
          </view>
        </view>
        <button class="delete-btn" @click="removeItem(item.id)">删除</button>
      </view>
    </view>
    
    <view class="cart-footer" v-if="cartStore.items.length > 0">
      <text class="total">合计: ¥{{ cartStore.totalPrice.toFixed(2) }}</text>
      <button type="primary" @click="checkout">结算 ({{ cartStore.totalCount }})</button>
    </view>
  </view>
</template>

<script setup lang="ts">
import { useCartStore } from '@/stores/cart'

const cartStore = useCartStore()

function increase(id: number) {
  const item = cartStore.items.find(i => i.id === id)
  if (item) cartStore.addItem(item, 1)
}

function decrease(id: number) {
  const item = cartStore.items.find(i => i.id === id)
  if (item) {
    if (item.count > 1) {
      item.count--
    } else {
      cartStore.removeItem(id)
    }
    uni.setStorageSync('cart', cartStore.items)
  }
}

function removeItem(id: number) {
  uni.showModal({
    title: '提示',
    content: '确定删除该商品?',
    success: (res) => {
      if (res.confirm) cartStore.removeItem(id)
    }
  })
}

function checkout() {
  uni.showToast({ title: '结算功能开发中', icon: 'none' })
}

function goHome() {
  uni.switchTab({ url: '/pages/index/index' })
}
</script>
    

6. 商品详情页面

<!-- pages/detail/index.vue -->
<template>
  <view class="container">
    <uni-nav-bar title="商品详情" left-icon="back" @click-left="goBack"></uni-nav-bar>
    
    <view class="detail">
      <image :src="product?.image" class="detail-image"></image>
      <view class="detail-info">
        <text class="detail-name">{{ product?.name }}</text>
        <text class="detail-price">¥{{ product?.price }}</text>
        <text class="detail-desc">{{ product?.description }}</text>
      </view>
    </view>
    
    <view class="action-bar">
      <button @click="addToCart(product!)">加入购物车</button>
      <button type="primary" @click="buyNow(product!)">立即购买</button>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useCartOperation } from '@/hooks/useCartOperation'
import type { Product } from '@/types'

const product = ref(null)
const { addToCart, buyNow } = useCartOperation()

onLoad((query) => {
  const id = Number(query.id)
  // 模拟根据ID获取商品详情
  const products: Product[] = [
    { id: 1, name: 'Vue3实战指南', price: 59.9, image: '/static/book1.png', description: '深入学习Vue3组合式API,掌握Composition API和响应式原理' },
    { id: 2, name: 'uni-app跨平台开发', price: 69.9, image: '/static/book2.png', description: '一套代码发布到iOS、Android、H5及小程序' },
  ]
  product.value = products.find(p => p.id === id) || null
})

function goBack() {
  uni.navigateBack()
}
</script>
    

7. 用户登录页面

<!-- pages/login/index.vue -->
<template>
  <view class="container">
    <uni-nav-bar title="登录"></uni-nav-bar>
    
    <view class="login-form">
      <uni-forms ref="form" :modelValue="formData">
        <uni-forms-item label="用户名" name="username">
          <uni-easyinput v-model="formData.username" placeholder="请输入用户名"></uni-easyinput>
        </uni-forms-item>
        <uni-forms-item label="密码" name="password">
          <uni-easyinput type="password" v-model="formData.password" placeholder="请输入密码"></uni-easyinput>
        </uni-forms-item>
      </uni-forms>
      
      <button type="primary" :loading="loading" @click="handleLogin">登录</button>
    </view>
  </view>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const loading = ref(false)
const formData = reactive({
  username: '',
  password: ''
})

async function handleLogin() {
  if (!formData.username || !formData.password) {
    uni.showToast({ title: '请填写完整信息', icon: 'none' })
    return
  }
  
  loading.value = true
  try {
    await userStore.login({
      username: formData.username,
      password: formData.password
    })
    uni.showToast({ title: '登录成功', icon: 'success' })
    uni.switchTab({ url: '/pages/user/index' })
  } catch (error) {
    uni.showToast({ title: '登录失败', icon: 'error' })
  } finally {
    loading.value = false
  }
}
</script>
    

四、核心机制详解

1. 组合式API与Hooks

我们创建了useProductListuseCartOperation两个自定义hooks,将商品列表加载和购物车操作逻辑封装为独立模块。这些hooks可以在多个页面中复用,且逻辑集中易于测试。

2. Pinia状态管理

useCartStoreuseUserStore分别管理购物车和用户状态。购物车数据通过uni.setStorageSync持久化到本地,应用启动时自动恢复。Pinia的模块化设计让状态管理清晰且类型安全。

3. 跨平台适配

uni-app的组件(如uni-nav-baruni-forms)自动适配不同平台。我们使用条件编译(#ifdef)处理平台差异,例如在微信小程序中使用buttonopen-type属性。

4. 组件通信

页面间通过uni.navigateTo传递参数,组件间通过Props和Events通信。全局状态通过Pinia共享,避免了多层组件传递的麻烦。

五、运行与测试

将代码保存到对应目录后,在HBuilderX中运行:

# 运行到H5
npm run dev:h5

# 运行到微信小程序
npm run dev:mp-weixin
    

在H5浏览器或微信开发者工具中测试:

  • 商品列表页展示所有商品,支持下拉刷新
  • 点击商品进入详情页,可加入购物车或立即购买
  • 购物车页面显示已添加的商品,支持数量修改和删除
  • 登录页面模拟用户认证,登录后显示用户信息

六、扩展:使用uni-ui组件库

uni-app官方提供了丰富的UI组件库uni-ui,我们已经在项目中使用了uni-nav-baruni-formsuni-load-more等组件。这些组件已针对多端适配,可以直接使用。

七、常见陷阱与最佳实践

  • 状态持久化:购物车等关键状态需要持久化到本地存储,避免应用重启后丢失
  • 平台差异:使用条件编译处理平台特有的API(如微信登录、支付)
  • 性能优化:列表页使用v-for时添加key,大数据列表使用虚拟滚动
  • 代码组织:将业务逻辑抽离到hooks和stores中,保持页面代码简洁

八、总结

通过构建电商应用,我们深入实践了uni-app的核心技术:

  • 组合式API:逻辑复用更灵活,代码组织更清晰
  • Pinia状态管理:模块化、类型安全的全局状态管理
  • 跨平台适配:一套代码运行多端
  • 组件化开发:使用uni-ui快速构建界面

这套技术栈特别适合中小团队快速开发跨平台应用,既能保证开发效率,又能获得良好的用户体验。


本文为原创技术教程,代码基于uni-app 3.0和Vue3测试通过。建议在实际项目中结合uni-push实现消息推送。

uni-app组合式API与状态管理实战:构建跨平台电商应用
收藏 (0) 打赏

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

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

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

淘吗网 uniapp uni-app组合式API与状态管理实战:构建跨平台电商应用 https://www.taomawang.com/web/uniapp/1801.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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