UniApp跨平台电商应用开发实战 – 从零构建完整购物应用

2025-08-21 0 1,002

项目概述与设计思路

本教程将带领大家使用UniApp开发一个功能完整的跨平台电商应用,涵盖首页展示、商品详情、购物车、订单管理、用户中心等核心功能。通过这个项目,您将掌握UniApp在多端开发中的核心技术,包括条件编译、组件通信、状态管理和第三方服务集成。

环境搭建与项目初始化

首先安装HBuilderX并创建UniApp项目:

# 使用HBuilderX可视化创建项目
1. 选择文件 -> 新建 -> 项目
2. 选择UniApp,填写项目名称
3. 选择默认模板
4. 点击创建

# 或者使用CLI方式(需安装vue-cli)
npm install -g @vue/cli
vue create -p dcloudio/uni-preset-vue my-project
cd my-project
npm install

项目目录结构规划

合理的目录结构是项目成功的基础:

project/
├── components/           # 公共组件
│   ├── product-card/    # 商品卡片组件
│   ├── search-bar/      # 搜索组件
│   └── tab-bar/         # 自定义标签栏
├── pages/               # 页面文件
│   ├── index/           # 首页
│   ├── category/        # 分类页
│   ├── cart/            # 购物车
│   ├── user/            # 用户中心
│   └── product-detail/  # 商品详情
├── static/              # 静态资源
├── store/               # 状态管理
├── api/                 # 接口管理
├── utils/               # 工具函数
└── uni.scss             # 全局样式配置

配置manifest.json实现多端适配

通过manifest.json配置各平台特性:

{
  "name": "电商商城",
  "appid": "__UNI__XXXXXX",
  "description": "跨平台电商应用",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": false,
  "mp-weixin": {
    "appid": "wxxxxxxxxxxxxxxxx",
    "setting": {
      "urlCheck": false
    },
    "usingComponents": true
  },
  "app-plus": {
    "usingComponents": true,
    "nvueStyle": "flex",
    "compilerVersion": 3,
    "splashscreen": {
      "autoclose": true,
      "waiting": true
    }
  },
  "h5": {
    "router": {
      "mode": "hash"
    },
    "template": "template.h5.html"
  }
}

Vuex状态管理设计

创建store管理全局状态:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    cartList: [], // 购物车数据
    userInfo: null, // 用户信息
    token: uni.getStorageSync('token') || '',
    historySearch: uni.getStorageSync('historySearch') || [] // 搜索历史
  },
  mutations: {
    // 添加至购物车
    ADD_TO_CART(state, product) {
      const existingItem = state.cartList.find(item => item.id === product.id)
      if (existingItem) {
        existingItem.quantity += product.quantity || 1
      } else {
        state.cartList.push({
          ...product,
          quantity: product.quantity || 1,
          selected: true
        })
      }
      uni.setStorageSync('cartList', state.cartList)
    },
    
    // 更新购物车商品数量
    UPDATE_CART_ITEM(state, { id, quantity }) {
      const item = state.cartList.find(item => item.id === id)
      if (item) {
        item.quantity = quantity
        uni.setStorageSync('cartList', state.cartList)
      }
    },
    
    // 设置用户信息
    SET_USER_INFO(state, userInfo) {
      state.userInfo = userInfo
      uni.setStorageSync('userInfo', userInfo)
    },
    
    // 设置token
    SET_TOKEN(state, token) {
      state.token = token
      uni.setStorageSync('token', token)
    }
  },
  actions: {
    // 登录动作
    async login({ commit }, loginData) {
      try {
        const res = await uni.request({
          url: '/api/user/login',
          method: 'POST',
          data: loginData
        })
        
        if (res.data.code === 200) {
          commit('SET_TOKEN', res.data.token)
          commit('SET_USER_INFO', res.data.userInfo)
          return Promise.resolve(res.data)
        } else {
          return Promise.reject(res.data.message)
        }
      } catch (error) {
        return Promise.reject(error)
      }
    }
  },
  getters: {
    // 计算购物车总数量
    cartTotalCount: state => {
      return state.cartList.reduce((total, item) => total + item.quantity, 0)
    },
    
    // 计算购物车总价格
    cartTotalPrice: state => {
      return state.cartList.reduce((total, item) => {
        return item.selected ? total + (item.price * item.quantity) : total
      }, 0)
    },
    
    // 选中的购物车商品
    selectedCartItems: state => {
      return state.cartList.filter(item => item.selected)
    }
  }
})

export default store

首页组件开发

实现电商应用首页:

<template>
  <view class="page-container">
    <!-- 自定义导航栏 -->
    <view class="custom-navbar">
      <view class="location" @click="chooseLocation">
        <text class="iconfont icon-location"></text>
        <text>{{ location || '选择位置' }}</text>
      </view>
      <view class="search-bar" @click="navigateToSearch">
        <text class="iconfont icon-search"></text>
        <text class="placeholder">搜索商品</text>
      </view>
    </view>
    
    <!-- 轮播图 -->
    <swiper class="banner-swiper" :autoplay="true" :interval="3000" circular>
      <swiper-item v-for="(item, index) in bannerList" :key="index">
        <image :src="item.image" mode="aspectFill" @click="handleBannerClick(item)"></image>
      </swiper-item>
    </swiper>
    
    <!-- 分类入口 -->
    <view class="category-entry">
      <view 
        v-for="category in categoryList" 
        :key="category.id"
        class="category-item"
        @click="navigateToCategory(category.id)"
      >
        <image :src="category.icon"></image>
        <text>{{ category.name }}</text>
      </view>
    </view>
    
    <!-- 推荐商品 -->
    <view class="recommend-section">
      <view class="section-header">
        <text class="title">热门推荐</text>
        <text class="more" @click="navigateToProductList">查看更多 ></text>
      </view>
      <view class="product-list">
        <product-card 
          v-for="product in productList" 
          :key="product.id" 
          :product="product"
          @click="navigateToProductDetail(product.id)"
        ></product-card>
      </view>
    </view>
  </view>
</template>

<script>
import { mapState, mapGetters } from 'vuex'
import ProductCard from '@/components/product-card/product-card.vue'

export default {
  components: {
    ProductCard
  },
  data() {
    return {
      bannerList: [],
      categoryList: [],
      productList: [],
      location: ''
    }
  },
  computed: {
    ...mapState(['userInfo']),
    ...mapGetters(['cartTotalCount'])
  },
  onLoad() {
    this.loadHomeData()
    this.getLocation()
  },
  onPullDownRefresh() {
    this.loadHomeData().finally(() => {
      uni.stopPullDownRefresh()
    })
  },
  methods: {
    // 加载首页数据
    async loadHomeData() {
      try {
        const [bannerRes, categoryRes, productRes] = await Promise.all([
          this.$api.getBannerList(),
          this.$api.getCategoryList(),
          this.$api.getProductList({ page: 1, limit: 8 })
        ])
        
        this.bannerList = bannerRes.data
        this.categoryList = categoryRes.data
        this.productList = productRes.data.list
      } catch (error) {
        uni.showToast({
          title: '加载失败',
          icon: 'none'
        })
      }
    },
    
    // 获取地理位置
    async getLocation() {
      try {
        const res = await uni.getLocation({
          type: 'wgs84'
        })
        
        // 逆地理编码获取具体位置
        const locationInfo = await this.$api.reverseGeocoder(res.latitude, res.longitude)
        this.location = locationInfo.address_component.city
      } catch (error) {
        console.log('获取位置失败')
      }
    },
    
    // 选择位置
    chooseLocation() {
      uni.chooseLocation({
        success: (res) => {
          this.location = res.name
        }
      })
    },
    
    // 跳转到搜索页
    navigateToSearch() {
      uni.navigateTo({
        url: '/pages/search/search'
      })
    },
    
    // 跳转到商品详情
    navigateToProductDetail(productId) {
      uni.navigateTo({
        url: `/pages/product-detail/product-detail?id=${productId}`
      })
    },
    
    // 处理轮播图点击
    handleBannerClick(banner) {
      if (banner.link_type === 'product') {
        this.navigateToProductDetail(banner.link_value)
      } else if (banner.link_type === 'category') {
        this.navigateToCategory(banner.link_value)
      }
    }
  }
}
</script>

商品详情页开发

实现商品详情页面:

<template>
  <view class="product-detail">
    <!-- 商品图片轮播 -->
    <swiper class="product-swiper" indicator-dots circular>
      <swiper-item v-for="(image, index) in productInfo.images" :key="index">
        <image :src="image" mode="aspectFit"></image>
      </swiper-item>
    </swiper>
    
    <!-- 商品信息 -->
    <view class="product-info">
      <view class="price-section">
        <text class="current-price">¥{{ productInfo.price }}</text>
        <text class="original-price" v-if="productInfo.original_price">
          ¥{{ productInfo.original_price }}
        </text>
        <text class="sales">已售{{ productInfo.sales_count }}件</text>
      </view>
      
      <view class="title-section">
        <text class="title">{{ productInfo.name }}</text>
        <view class="collect" @click="toggleCollect">
          <text class="iconfont" :class="isCollected ? 'icon-collect-fill' : 'icon-collect'"></text>
        </view>
      </view>
      
      <view class="spec-section">
        <text class="label">规格</text>
        <view class="spec-list">
          <text 
            v-for="spec in productInfo.specs" 
            :key="spec" 
            class="spec-item"
            :class="{ active: selectedSpec === spec }"
            @click="selectedSpec = spec"
          >
            {{ spec }}
          </text>
        </view>
      </view>
    </view>
    
    <!-- 商品详情 -->
    <view class="detail-section">
      <view class="section-title">商品详情</view>
      <rich-text :nodes="productInfo.detail"></rich-text>
    </view>
    
    <!-- 底部操作栏 -->
    <view class="action-bar">
      <view class="action-item" @click="navigateToHome">
        <text class="iconfont icon-home"></text>
        <text>首页</text>
      </view>
      
      <view class="action-item" @click="navigateToCart">
        <text class="iconfont icon-cart"></text>
        <text>购物车</text>
        <text class="badge" v-if="cartCount > 0">{{ cartCount }}</text>
      </view>
      
      <view class="btn add-cart" @click="addToCart">
        加入购物车
      </view>
      
      <view class="btn buy-now" @click="buyNow">
        立即购买
      </view>
    </view>
  </view>
</template>

<script>
import { mapState, mapMutations, mapGetters } from 'vuex'

export default {
  data() {
    return {
      productInfo: {},
      selectedSpec: '',
      isCollected: false,
      productId: ''
    }
  },
  computed: {
    ...mapState(['cartList']),
    ...mapGetters(['cartTotalCount']),
    cartCount() {
      return this.cartTotalCount
    }
  },
  onLoad(options) {
    this.productId = options.id
    this.loadProductDetail()
    this.checkCollectionStatus()
  },
  methods: {
    ...mapMutations(['ADD_TO_CART']),
    
    // 加载商品详情
    async loadProductDetail() {
      try {
        const res = await this.$api.getProductDetail(this.productId)
        this.productInfo = res.data
        this.selectedSpec = this.productInfo.specs[0] || ''
      } catch (error) {
        uni.showToast({
          title: '加载失败',
          icon: 'none'
        })
      }
    },
    
    // 检查收藏状态
    async checkCollectionStatus() {
      if (!this.$store.state.token) return
      
      try {
        const res = await this.$api.checkCollection(this.productId)
        this.isCollected = res.data.collected
      } catch (error) {
        console.log('检查收藏状态失败')
      }
    },
    
    // 切换收藏状态
    async toggleCollect() {
      if (!this.$store.state.token) {
        uni.navigateTo({
          url: '/pages/login/login'
        })
        return
      }
      
      try {
        if (this.isCollected) {
          await this.$api.removeCollection(this.productId)
          uni.showToast({
            title: '取消收藏',
            icon: 'success'
          })
        } else {
          await this.$api.addCollection(this.productId)
          uni.showToast({
            title: '收藏成功',
            icon: 'success'
          })
        }
        this.isCollected = !this.isCollected
      } catch (error) {
        uni.showToast({
          title: '操作失败',
          icon: 'none'
        })
      }
    },
    
    // 添加到购物车
    addToCart() {
      const cartItem = {
        id: this.productInfo.id,
        name: this.productInfo.name,
        price: this.productInfo.price,
        image: this.productInfo.images[0],
        spec: this.selectedSpec,
        quantity: 1
      }
      
      this.ADD_TO_CART(cartItem)
      uni.showToast({
        title: '添加成功',
        icon: 'success'
      })
    },
    
    // 立即购买
    buyNow() {
      const orderItem = {
        id: this.productInfo.id,
        name: this.productInfo.name,
        price: this.productInfo.price,
        image: this.productInfo.images[0],
        spec: this.selectedSpec,
        quantity: 1
      }
      
      uni.navigateTo({
        url: `/pages/order-confirm/order-confirm?items=${JSON.stringify([orderItem])}`
      })
    },
    
    navigateToHome() {
      uni.switchTab({
        url: '/pages/index/index'
      })
    },
    
    navigateToCart() {
      uni.switchTab({
        url: '/pages/cart/cart'
      })
    }
  }
}
</script>

多端适配与条件编译

使用条件编译处理平台差异:

// 平台特定代码处理
export function share(content) {
  // #ifdef MP-WEIXIN
  return wx.shareAppMessage(content)
  // #endif
  
  // #ifdef APP-PLUS
  let shares = []
  if (content.shareType === 0) {
    shares = [{
      type: 'text',
      content: content.title
    }]
  } else {
    shares = [{
      type: 'image',
      content: content.imageUrl
    }]
  }
  plus.share.sendWithSystem({
    type: 'web',
    title: content.title,
    content: content.desc,
    href: content.path,
    thumbs: [content.imageUrl]
  })
  // #endif
  
  // #ifdef H5
  if (navigator.share) {
    navigator.share({
      title: content.title,
      text: content.desc,
      url: window.location.origin + content.path
    })
  } else {
    uni.showModal({
      content: '请手动分享链接: ' + window.location.origin + content.path
    })
  }
  // #endif
}

// 平台特定的样式处理
const getPlatformStyle = () => {
  // #ifdef MP-WEIXIN
  return {
    paddingTop: '44px'
  }
  // #endif
  
  // #ifdef APP-PLUS
  return {
    paddingTop: '64px'
  }
  // #endif
  
  // #ifdef H5
  return {
    paddingTop: '0'
  }
  // #endif
}

性能优化策略

实施以下优化策略提升应用性能:

// 1. 图片懒加载
<image 
  :src="item.image" 
  mode="aspectFill" 
  lazy-load
  :fade-show="false"
></image>

// 2. 数据缓存策略
async getProductDetail(id) {
  const cacheKey = `product_${id}`
  const cacheData = uni.getStorageSync(cacheKey)
  
  if (cacheData && Date.now() - cacheData.timestamp  import('@/components/product-card/product-card.vue')

// 4. 减少setData调用频率
let updateTimer = null
function debounceUpdate(data) {
  if (updateTimer) clearTimeout(updateTimer)
  updateTimer = setTimeout(() => {
    this.setData(data)
  }, 100)
}

// 5. 使用wxs处理复杂计算(微信小程序)
// <wxs module="math" src="./math.wxs"></wxs>
// <view>{{ math.calculatePrice(price, quantity) }}</view>

项目发布与部署

各平台发布流程:

# 微信小程序发布
1. 在HBuilderX中选择 发行 -> 小程序-微信
2. 输入小程序appid
3. 生成开发版,通过微信开发者工具上传
4. 在微信公众平台提交审核

# App发布
1. 选择 发行 -> 原生App-云打包
2. 选择证书配置(首次需要生成证书)
3. 选择打包平台(iOS/Android)
4. 等待打包完成,下载安装包

# H5发布
1. 选择 发行 -> 网站-H5手机版
2. 配置网站标题和域名
3. 生成静态文件,部署到服务器

# 配置持续集成(可选)
在项目根目录创建.github/workflows/deploy.yml:
name: Deploy UniApp
on:
  push:
    branches: [ main ]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '14'
    - name: Install dependencies
      run: npm install
    - name: Build for H5
      run: npm run build:h5
    - name: Deploy to server
      uses: peaceiris/actions-gh-pages@v3
      with:
        deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
        publish_dir: ./dist/build/h5

总结

通过本教程,我们完成了一个功能完整的跨平台电商应用,涵盖了UniApp开发的核心技术和最佳实践:

  • 基于Vue的组件化开发模式
  • Vuex状态管理和数据持久化
  • 多端适配和条件编译技巧
  • 性能优化和用户体验提升策略
  • 各平台发布和部署流程

UniApp的强大之处在于其”一次开发,多端发布”的能力,大大提高了开发效率。希望本教程能帮助您掌握UniApp开发技能,构建出优秀的跨平台应用。

UniApp跨平台电商应用开发实战 - 从零构建完整购物应用
收藏 (0) 打赏

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

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

淘吗网 uniapp UniApp跨平台电商应用开发实战 – 从零构建完整购物应用 https://www.taomawang.com/web/uniapp/940.html

常见问题

相关文章

发表评论
暂无评论
官方客服团队

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