项目概述与设计思路
本教程将带领大家使用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开发技能,构建出优秀的跨平台应用。