做电商项目,最怕的不是业务逻辑复杂,而是“一套代码,多处运行”时的水土不服。很多开发者刚开始觉得 UniApp 真香,写一次能发微信小程序、H5、甚至 App,结果到了 App 端就发现启动慢、内存爆、样式错乱,最后不得不拆分团队,搞原生开发。
今天我们就聊聊怎么把这个坑填平。我不讲虚的理论,直接上实战中踩过的雷和填坑的经验。我们要做的,是一个既能跑在微信里流畅滑动,又能打包成独立 App 稳定运行的电商系统。
一、 架构设计:不仅仅是页面堆砌
很多新手一上来就建 Page,这是大忌。电商项目涉及商品详情、购物车、订单、支付、个人中心等多个模块,状态管理如果不做好,后期维护就是灾难。
1. 目录结构的规范
别把所有 .vue 文件都扔在 pages 目录下。我建议采用模块化目录结构:
src/
├── api/ # 接口请求封装
├── components/ # 公共组件(如:商品卡片、底部导航、搜索栏)
├── pages/ # 页面文件
│ ├── index/ # 首页
│ ├── goods/ # 商品相关
│ └── order/ # 订单相关
├── static/ # 静态资源
├── store/ # Pinia/Vuex 状态管理
├── utils/ # 工具函数(日期格式化、金额处理)
└── App.vue # 入口组件
2. 状态管理的选择
对于电商项目,购物车数据、用户登录状态、商品筛选条件等需要全局共享。推荐使用 Pinia(Vue 3)或 Vuex(Vue 2)。UniApp 官方现在更推荐 Pinia,因为它更轻量,且对 TypeScript 支持更好。
关键点: 不要把大量数据存在 store 里而不做持久化。比如用户登录态,必须同步到本地 Storage,否则用户杀进程后重新打开,还得重新登录。
// store/modules/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getUserInfoApi } from '@/api/user'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
const login = async (code) => {
// 模拟微信登录流程
const res = await getUserInfoApi(code)
token.value = res.token
userInfo.value = res.userInfo
localStorage.setItem('token', res.token)
localStorage.setItem('userInfo', JSON.stringify(res.userInfo))
}
const logout = () => {
token.value = ''
userInfo.value = {}
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
return { token, userInfo, login, logout }
})
二、 核心功能实战:商品列表与详情页
电商的核心是“看”和“买”。这里有两个难点:一是长列表的性能,二是图片加载速度。
1. 虚拟列表解决长列表卡顿
微信小程序和 App 对 DOM 节点数量敏感。如果你的商品列表有几百个,直接渲染会导致页面卡顿。UniApp 提供了 <recycle-list> 组件(需开启虚拟列表模式),或者使用 mpvue-picker 等第三方方案,但最简单有效的是利用 CSS 的 overflow 配合固定高度滚动,或者使用 uni-list 组件。
但在 App 端,uni-list 有时表现不如原生 scroll-view。建议针对 App 和小程序做条件编译。
代码示例:条件编译优化列表渲染
<template>
<!-- 小程序端使用 uni-list,性能好且兼容性好 -->
<view v-if="platform === 'mp-weixin'">
<uni-list :list="goodsList">
<uni-list-item
v-for="item in goodsList"
:key="item.id"
:title="item.name"
:note="item.price"
@click="goDetail(item)"
/>
</uni-list>
</view>
<!-- App 端使用原生 scroll-view + 自定义卡片,更灵活 -->
<view v-else-if="platform === 'app'" class="app-goods-list">
<scroll-view scroll-y class="list-container">
<view class="goods-card" v-for="item in goodsList" :key="item.id" @click="goDetail(item)">
<image :src="item.image" mode="aspectFill" class="goods-img"></image>
<view class="goods-info">
<text class="title">{{ item.name }}</text>
<text class="price">¥{{ item.price }}</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const platform = computed(() => {
// #ifdef MP-WEIXIN
return 'mp-weixin'
// #endif
// #ifdef APP-PLUS
return 'app'
// #endif
return 'h5'
})
// ... 其他逻辑
</script>
<style lang="scss">
/* App 端特有样式 */
.app-goods-list {
height: 100vh;
.list-container {
height: 100%;
}
.goods-card {
display: flex;
padding: 20rpx;
background: #fff;
margin-bottom: 10rpx;
.goods-img {
width: 200rpx;
height: 200rpx;
border-radius: 10rpx;
}
.goods-info {
margin-left: 20rpx;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
}
</style>
2. 图片懒加载与预加载
电商页面图片多,必须懒加载。UniApp 的 <image> 组件默认支持懒加载,但可以通过 lazy-load 属性显式开启。
进阶技巧: 在商品列表页,不仅当前屏的图片要加载,还要预判用户可能点击的商品,提前加载其详情页的大图。这可以通过监听列表滚动位置来实现。
// 伪代码:滚动时预加载下一屏图片
onPageScroll((e) => {
const scrollTop = e.scrollTop
const windowHeight = uni.getSystemInfoSync().windowHeight
// 计算当前可视区域下方的商品 ID 列表
const visibleIds = getVisibleGoodsIds(scrollTop, windowHeight)
const nextBatchIds = getNextBatch(visibleIds)
// 触发预加载
preLoadImages(nextBatchIds)
})
三、 跨平台部署方案:从配置到发布
很多人以为 UniApp 就是“写一次,到处跑”,其实不然。小程序和 App 的审核机制、权限申请、包体积限制完全不同。
1. 环境配置区分
在 manifest.json 中,你需要分别配置 mp-weixin 和 app-plus 节点。
- 小程序: 重点配置
appid、setting(如地图 key、上传文件限制)、permission(如获取用户信息、位置)。 - App: 重点配置
distribute(图标、启动图、权限声明)、sdkConfigs(推送、分享、支付 SDK 配置)。
注意: App 的包体积限制很严。iOS 上架前最好控制在 200MB 以内(虽然 Apple 允许更大,但下载转化率会下降),Android 也要避免过大。
2. 原生插件的使用
UniApp 的强大之处在于可以调用原生能力。比如电商常用的“微信支付”、“支付宝支付”、“地图定位”、“人脸识别实名认证”。
不要试图用 JS 去模拟这些操作,直接用 DCloud 插件市场里的原生插件。
示例:集成微信支付插件
- 在插件市场下载
uni-pay或官方推荐的payment插件。 - 在
manifest.json->App 模块配置->支付中勾选微信支付。 - 在代码中调用:
import payment from '@/uni_modules/uni-pay/js_sdk/payment'
async function wxPay(orderId) {
try {
// 1. 后端下单,获取支付参数
const payParams = await createOrderApi(orderId)
// 2. 调用支付
const res = await payment.pay({
provider: 'wxpay',
orderInfo: payParams.orderInfo // 后端返回的签名后的支付参数
})
console.log('支付成功', res)
} catch (err) {
console.error('支付失败', err)
}
}
3. H5 与 App 的差异化处理
H5 端通常没有复杂的原生权限,但要注意 SEO 和首屏加载速度。App 端则要注意离线打包和热更新。
热更新策略: 在 App 端,每次发版都要过应用商店审核,太慢。可以使用云打包或离线打包结合热更新服务(如 DCloud 的云热更或第三方服务)。
// 检查更新
function checkUpdate() {
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, (wgtinfo) => {
uni.request({
url: 'https://your-api.com/check-update',
success: (res) => {
if (res.data.version > wgtinfo.version) {
// 提示用户更新
uni.showModal({
title: '版本更新',
content: '发现新版本,是否下载更新?',
success: (modalRes) => {
if (modalRes.confirm) {
downloadAndUpdate(res.data.downloadUrl)
}
}
})
}
}
})
})
// #endif
}
四、 性能优化技巧:让 App 飞起来
这是最关键的部分。UniApp 打包成 App 后,本质上是一个 WebView 容器,性能瓶颈主要在 JS 线程和 UI 渲染。
1. 减少 setData 频率
在小程序和 App 中,频繁调用 this.$apply() 或 setData 会导致主线程阻塞。
错误做法:
// 每收到一个消息就更新列表
onMessage(msg) {
this.list.push(msg)
this.$forceUpdate() // 或 setData
}
正确做法: 使用节流,或者只在必要时更新。对于实时性要求不高的场景,可以批量更新。
2. 组件懒加载
对于非首屏的页面或大型组件,使用动态导入。
// pages/order/order.vue
export default {
components: {
OrderItem: () => import('@/components/order/OrderItem.vue')
}
}
3. 图片优化
- 格式: 优先使用 WebP 格式,体积小且清晰。UniApp 在 App 端对 WebP 支持良好。
- 压缩: 上传前在前端压缩图片,或使用 CDN 的图像处理功能(如阿里云 OSS 的参数)。
- 占位图: 图片未加载出来时,显示灰色占位图,避免布局抖动。
4. 启动速度优化
App 冷启动慢是通病。
- 分包加载: 将非核心页面(如“我的”、“设置”)放入分包。在主包
pages.json中配置subPackages。 - 预加载: 在首页加载完成后,异步请求其他页面的数据。
- 精简代码: 移除未使用的插件和库。
分包配置示例:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
}
],
"subPackages": [
{
"root": "packageOrder",
"pages": [
{
"path": "create/create",
"style": {
"navigationBarTitleText": "创建订单"
}
}
]
}
]
}
5. 内存泄漏排查
电商 App 长时间运行容易内存溢出。
- 清理定时器: 页面销毁时,务必清除
setInterval和setTimeout。 - 解绑事件: 监听全局事件(如网络变化)时,记得在
onUnload中取消监听。 - 图片释放: 大图加载后,如果不再使用,可以考虑置空引用。
onUnload() {
// #ifdef APP-PLUS
// 清理资源
this.timer && clearInterval(this.timer)
this.timer = null
// #endif
}
五、 常见问题与避坑指南
1. 样式兼容性问题
- Flex 布局: 小程序和 App 对 Flex 的支持基本一致,但要注意
align-items在某些旧版本 Android 上的表现。 - 单位: 建议使用
rpx,它会自动适配屏幕宽度。但在 App 端,如果涉及精确的原生控件尺寸,可能需要转换为px。 - Z-index: 小程序和 App 的层级管理不同。App 端有时会出现弹窗被遮挡的情况,可以尝试给弹窗添加
z-index: 9999或使用fixed定位。
2. 网络请求封装
统一封装 request 方法,处理 Token 过期、错误码拦截。
// utils/request.js
export function request(url, method = 'GET', data = {}) {
return new Promise((resolve, reject) => {
uni.request({
url: `${baseUrl}${url}`,
method,
data,
header: {
'Authorization': `Bearer ${getToken()}`
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else if (res.statusCode === 401) {
// 处理 token 过期,跳转登录
handleLogout()
reject(new Error('Unauthorized'))
} else {
reject(new Error('Network error'))
}
},
fail: (err) => {
reject(err)
}
})
})
}
3. 真机调试的重要性
模拟器永远无法完全代表真机。特别是 App 端,不同品牌手机(华为、小米、OPPO、iPhone)的系统差异巨大。
- 必测机型: iPhone SE(小屏)、iPhone Pro Max(大屏)、低端 Android(性能差)、高端 Android(性能强)。
- 必测场景: 弱网环境(开启开发者工具的 Network Throttling)、后台切换回来、来电中断。
六、 总结
开发一个成功的电商 UniApp 项目,不是简单地“写完代码”,而是从架构设计、性能优化、跨平台适配到最终发布的全流程把控。
- 架构先行: 模块化、状态管理、目录规范。
- 性能为王: 虚拟列表、图片优化、分包加载、减少 setData。
- 原生结合: 合理使用原生插件,弥补 JS 能力的不足。
- 真机验证: 覆盖主流机型和场景,确保稳定性。
记住,没有完美的框架,只有优秀的开发者。UniApp 给了你一把瑞士军刀,但怎么用,取决于你对业务场景的理解和对细节的打磨。希望这份实战指南能帮你少走弯路,做出既好看又好用的电商应用。
