十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于uni-app的微信小程序电商购物平台开发实战

基于uni-app的微信小程序电商购物平台开发实战 这两年帮人带过不少电商类的小程序项目我自己最直观的感受是以“微信小程序 uni-app”为技术栈去实现一个在线购物平台放在课程设计、毕业设计或者个人作品集里是整个移动端方向里性价比最高的题目之一。需求明确、技术点覆盖全、做完以后真能跑起来而且无论以后走前端还是全栈方向这套项目的复用性都很强。这篇文章我就按照自己实际带项目的思路把在线购物平台从设计、选型、核心模块实现到调试和常见问题排查整个链路完整写一遍。内容不会只停留在“页面长什么样”而是会重点讲清楚每个设计决策背后的逻辑以及哪些地方容易踩坑。适合正在做相关课程设计的学生也适合想独立练手一个完整小程序项目的开发者参考。1. 项目整体设计与技术选型为什么是uni-app1.1 三个候选方案的横向对比很多人在技术选型上会纠结很久。这里我直接说结论如果目标是“做一套完整的在线购物平台”首选uni-app。微信原生小程序当然可以它的性能上限很高而且官方组件和API支持得最彻底。但问题是代码只能在微信生态里用将来想扩展到H5、支付宝小程序或者App端基本等于重写。Taro是另一套跨端方案缺点是它基于React语法对大多数只熟悉Vue的开发者来说上手成本会明显高一些。uni-app基于Vue语法国内社区非常活跃遇到问题搜得到方案这个优势在做实际项目时极其重要。打个比方原生小程序像自己开一个独立铺面装修完全按这个铺面来做很精致但想换个商场开分店所有东西都要翻新一遍uni-app更像连锁店体系一套设计稿换块招牌就能在不同平台开起来。对于课程设计、毕业设计和个人的第一个完整项目uni-app显然是一条更容易出成果的路线。1.2 整体架构与核心模块这套在线购物平台最核心的链路就是“人、货、单”三个字。人是用户与登录体系货是商品展示与分类检索单是购物车、订单与交易状态管理。前端部分直接用uni-app编写用HBuilderX编译成微信小程序。后端可选方案比较多可以用Node.js的Express/Koa可以用Java Spring Boot也可以直接用uni-app官方配套的uniCloud云开发。我带项目的过程中用得比较多的是Node.js因为前后端语言一致学生看代码、改接口都更快一些。如果是为了省去服务器部署的麻烦uniCloud也完全够用。无论后端用什么语言接口层面至少要覆盖这些模块接口说明用户POST /api/user/login微信登录换取token商品GET /api/goods/list商品列表支持分类/分页/搜索商品GET /api/goods/detail商品详情购物车POST /api/cart/add加入购物车购物车GET /api/cart/list获取购物车购物车PUT /api/cart/update修改数量或选中状态购物车DELETE /api/cart/delete删除条目订单POST /api/order/create创建订单订单GET /api/order/list订单列表订单POST /api/order/pay模拟支付或真实支付地址POST /api/address/add新增收货地址地址GET /api/address/list地址列表需要注意的一点是不要为了控制工作量把地址模块砍掉。在线购物平台如果没有收货地址订单流程就缺了一环答辩时很容易被问到“你的订单怎么发货”。就算不做复杂的地址管理至少数据库表里要留字段下单时能让用户填写收货人、电话、地址这会让整个系统在逻辑上严谨很多。前端页面方面底部tabBar固定4个页签首页、分类、购物车、我的。此外还要有商品列表页、商品详情页、确认订单页、订单列表页、订单详情页和登录页。这里要记住外挂页面不要放进tabBar不然页面切换逻辑会非常别扭。1.3 后端表结构设计的“为什么”我把核心表结构先列出来后面所有功能都是围绕这些表展开的表名主要字段说明userid, openid, nickname, avatar, phone, created_at用户表openid唯一索引categoryid, name, parent_id, sort分类表支持二级分类goodsid, category_id, title, cover, images, price, stock, sales, status商品表status区分上架/下架cartid, user_id, goods_id, count, selected, created_at购物车表user_id goods_id做唯一索引orderid, order_no, user_id, total_price, status, address_id, created_at订单主表order_itemid, order_id, goods_id, goods_title, goods_cover, price, count订单明细表addressid, user_id, name, phone, province, city, district, detail收货地址表订单为什么要拆成order和order_item两张表这是电商领域的经典设计叫“主表 明细表”。主表存一笔订单的整体信息比如订单号、总金额、状态明细表存这笔订单里具体包含哪几个商品、每个商品买了多少件、当时成交价是多少。如果只塞在一张表里字段会非常冗余也很难做退款、开票、物流拆分这些扩展。以后要统计“哪个商品卖得好”直接从明细表聚合效率也更高。这个点写进设计文档是很加分的。2. 核心页面和基础配置的实现细节2.1 项目初始化、pages.json与tabBar如果你直接用HBuilderX创建项目时选择“uni-app - 默认模板”就行。命令行方式也可以创建但HBuilderX对“一键运行到微信开发者工具”的支持更好对刚上手的人最友好。pages.json是整个小程序的导航与窗口配置文件很多人一开始不重视它结果页面一多就乱了。这里有一个关键点pages数组里的第一项就是小程序的启动页一般把首页放在第一项同时在第一页的style里设置navigationBarBackgroundColor、navigationBarTextStyle和backgroundColor这样进入小程序时不会出现明显的白色闪屏体验会好很多。tabBar是购物平台的“骨架”我的配置大概是这样的tabBar: { color: #999999, selectedColor: #e93b3d, list: [ { pagePath: pages/index/index, text: 首页, iconPath: static/tab/home.png, selectedIconPath: static/tab/home-active.png }, { pagePath: pages/category/category, text: 分类, iconPath: static/tab/category.png, selectedIconPath: static/tab/category-active.png }, { pagePath: pages/cart/cart, text: 购物车, iconPath: static/tab/cart.png, selectedIconPath: static/tab/cart-active.png }, { pagePath: pages/mine/mine, text: 我的, iconPath: static/tab/mine.png, selectedIconPath: static/tab/mine-active.png } ] }要注意tabBar的text最多4个字icon用png格式尺寸建议81x81像素太大太小在部分机型上都会出现模糊问题。实际开发中不要用纯色透明底的复杂图标简单的线稿在选中和未选中状态下会更清晰。经常有人找我问顶部导航栏高度的问题。如果你想自定义导航栏不能写死高度。不同机型的状态栏高度不一样iPhone刘海屏和普通安卓机的差异非常大。我常用的计算方法是const systemInfo uni.getSystemInfoSync() const menuButtonInfo uni.getMenuButtonBoundingClientRect() const statusBarHeight systemInfo.statusBarHeight const navigationBarHeight (menuButtonInfo.top - statusBarHeight) * 2 menuButtonInfo.height这个公式的原理是菜单胶囊按钮在导航栏里大致垂直居中所以导航栏总高度 状态栏高度 胶囊顶部到状态栏的距离乘以2 胶囊按钮本身的高度。建议把这段封装成一个公共方法所有自定义导航栏页面统一调用。否则你会在iPhone上写完标题位置换到安卓上看标题直接顶到状态栏被测试提一堆bug。2.2 商品、购物车、订单页面的关键交互商品列表页一般用页面级的onReachBottom做分页加载。这里有坑如果你把商品列表封装成了组件onReachBottom这个页面生命周期不会自动在组件里触发。正确做法是在页面里监听onReachBottom然后通过ref调用组件内部加载下一页的方法。很多新手在这里折腾半天最后选择把分页逻辑写回页面里反而是更省事的方式。商品详情页的加购按钮一定要做防重复点击。用户快速点两下请求发两次购物车里就会出现两件同样的商品。我通常用一个adding布尔值加锁if (this.adding) return this.adding true try { await addToCart({ goodsId: this.goodsId, count: this.count }) } finally { this.adding false }购物车页的核心是选中状态和价格合计。合计金额最好用computed计算属性来做不要在模板里写复杂的表达式computed: { totalAmount() { return this.cartList .filter(item item.selected) .reduce((sum, item) sum item.price * item.count, 0) } }这里我额外强调一个安全原则下单时前端传给后端的商品数据只传商品ID和数量绝对不能传价格。后端必须根据商品ID去数据库重新读取最新价格再计算订单总金额。前端传的价格只用于展示如果后端直接信任别人改一下请求参数5块钱的商品就能变成0.01元下单。这个点在答辩时主动提“服务端二次校验价格”面试官和老师都会觉得你考虑得很全面。订单状态建议用int字段表示按业务生命周期递增0已取消、10待付款、20待发货、30待收货、40已完成。用数字的好处是排序和筛选都方便以后加退款状态可以插在对应阶段中间比如25退款中不会打乱已有逻辑。热搜里有人问“微信小程序单选框怎么实现”在订单确认页选择配送方式时确实会用到。我建议直接用原生radio-group包radio虽然样式不够华丽但兼容性最好。用label标签把radio和文字包一层能扩大点击区域label classradio-wrapper radio value1 color#e93b3d / text顺丰速运/text /label还有人问“长按拖拽滚动”这个在购物平台里其实不常用但如果个人中心要做“我关注的商品”排序或者后台要调整分类顺序就会用到。实现思路有两个一是用movable-area movable-view适合单个元素的拖拽二是监听touchstart、touchmove、touchend在move阶段实时交换数组元素。第二种更灵活但一定要做节流否则小程序渲染会卡到怀疑人生。3. 前后端数据交互登录、商品与订单闭环3.1 登录流程到底怎么设计登录是很多课程设计最容易糊弄的环节有的直接把用户昵称头像存在本地根本不往后端传。这样做的问题很明显用户换台手机或者清了缓存登录态就没了而且后端完全没有用户概念订单、购物车都无从关联。标准的微信小程序登录流程是这样的前端调用uni.login()拿到一个临时的code把code传给自己的后端后端拿code appid secret去调用微信的code2session接口微信返回openid和session_key后端根据openid查找用户不存在就新建一条用户记录后端生成自己系统的token返回给前端前端把token存到uni.setStorageSync(token, token)后续所有请求带上这里有一个红线必须强调appid和secret是后端配置绝不能写在小程序前端代码里。小程序前端代码本质上是可以被拿到本地的如果secret写死在前端等于把账号密钥公开了。token过期也要提前处理。我在项目里和后端约定token无效或过期时返回401前端统一在request.js里拦截清除本地token并跳转到登录页。不要在每个页面里重复写一大段跳转逻辑很容易漏。3.2 封装request请求层为什么每个uni-app项目都要封装一个request.js因为如果不封装每个页面里都会出现几十行重复的uni.request代码。改一次baseURL或者header得全项目搜索替换痛苦到怀疑人生。我常用的封装是带Promise版本的// utils/request.js const BASE_URL https://api.example.com export function request({ url, method GET, data {} }) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { if (res.statusCode 200) { const body res.data if (body.code 0) { resolve(body.data) } else if (body.code 401) { uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) reject(new Error(登录已过期)) } else { uni.showToast({ title: body.msg || 请求失败, icon: none }) reject(new Error(body.msg || 请求失败)) } } else { reject(new Error(HTTP res.statusCode)) } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) }统一约定后端返回结构为{ code, msg, data }code为0表示成功非0表示业务错误401表示登录态失效。这样业务层只需要写const data await request({ url: /api/goods/list, data: { page: 1 } })看起来非常干净。但要注意在页面methods里使用await时方法本身要加async并且用try/catch包住。否则一旦接口报错Promise被reject控制台会提示UnhandledPromiseRejection真机上页面会像卡死一样不响应排查起来特别恼火。3.3 核心业务接口设计接口设计要RESTful一点不要用“/getGoodsList”这种动词式命名直接“GET /api/goods/list”风格更好。分页参数我习惯用page和pageSize后端返回时要包含总条数total这样前端能正确判断还有没有下一页。商品列表接口的完整响应结构我习惯这样设计{ code: 0, data: { list: [ { id: 1, title: 商品标题, cover: https://cdn.xxx.com/cover.jpg, price: 99.00, sales: 200 } ], page: 1, pageSize: 10, total: 156 } }总条数total用全局计数查询出来虽然数据量大时会慢但课程设计阶段完全够用。如果你以后要做高并发再考虑缓存和搜索引擎现在不用过度设计。创建订单接口要注意前端传的商品列表应该是数组比如{ addressId: 3, goodsList: [ { goodsId: 11, count: 2 }, { goodsId: 15, count: 1 } ] }后端拿到这个数组后循环查询商品表用数据库里的实时价格乘以数量累加出total_price再生成唯一订单号最后在order表插入主记录、在order_item表插入明细记录。整个操作要在数据库事务里执行否则中途某个商品库存不足主表插进去了明细表没插全数据就脏了。3.4 从购物车到订单的完整流程示例下面给一个前端关键逻辑示例演示从购物车勾选商品到创建订单的完整过程。在确认订单页进入时通过事件通道或者页面参数拿到选中的商品数据和地址信息。提交订单的方法核心代码如下async submitOrder() { if (!this.addressId) { uni.showToast({ title: 请选择收货地址, icon: none }) return } if (this.buying) return this.buying true try { const res await request({ url: /api/order/create, method: POST, data: { addressId: this.addressId, goodsList: this.selectedGoods.map(item ({ goodsId: item.goodsId, count: item.count })) } }) const orderId res.orderId await request({ url: /api/order/pay, method: POST, data: { orderId } }) uni.redirectTo({ url: /pages/order/detail?id orderId }) } catch (e) { console.error(创建订单失败, e) } finally { this.buying false } }注意几个细节请求体只传商品ID和数量不传单价和总价价格由后端重新计算。支付环节课程设计用“模拟支付”就行点击按钮直接把订单状态从待付款改成待发货。如果有真实微信支付资质下单之后需要调用uni.requestPayment参数由后端统一下单后返回流程复杂很多但原理是通的。创建订单成功后要通知购物车页清除对应已购条目。我一般用uni.$emit触发购物车页的刷新方法或者让购物车页在onShow时重新拉取列表。防重复提交的this.buying锁非常关键不加的话用户快速点两次后端就可能创建两笔一模一样的订单。4. 性能优化、调试与常见问题排查4.1 小程序首屏优化与分包加载微信小程序主包有体积限制uni-app打包后如果所有图片都塞进static目录很容易超限。图片尽量传到云存储或者CDN数据库里存URL小程序里显示时直接加载远程地址。另一个提升项目完成度的操作是“分包加载”。pages.json里主包只保留tabBar对应的4个页面其他页面拆进subPackagessubPackages: [ { root: packageGoods, pages: [ { path: goods-detail/goods-detail }, { path: goods-list/goods-list } ] }, { root: packageOrder, pages: [ { path: order/confirm/confirm }, { path: order/list/list }, { path: order/detail/detail } ] } ]分包之后页面跳转的路径要带上root前缀比如“/packageGoods/goods-detail/goods-detail?idxxx”。而且分包页面不能使用tabBar相关的页面栈操作否则容易报找不到页面。首次进入小程序时主包变小加载速度会明显提升。首屏体验还可以做骨架屏。商品列表在数据没返回时先渲染一排灰色占位块等数据到了再替换成真实内容。这个技术不难但很能体现工程意识答辩时拿出来讲会加分。4.2 常见的uni-app报错与解决思路搜索热词里出现频率最高的一条是“Uncaught TypeError: Cannot read properties of undefined (reading xxx)”。这个错误的本质是访问了undefined对象的属性。常见场景有两个第一个是后端返回的数据结构和前端预期不一致。比如前端写res.data.list但res.data本身是个数组。我建议在遍历前加一层判断const list (res res.list) ? res.list : []第二个是this指向问题。在小程序里用setTimeout或uni.request的回调时里面的this不指向页面实例。解决方式有两种在外面先把this存成that或者直接用箭头函数onLoad() { const that this uni.request({ success(res) { that.goodsList res.data.list } }) }还有一类报错是“页面找不到”。pages.json里没有注册页面就直接uni.navigateTo跳转运行时就会提示。排查方法很简单打开pages.json逐个对照跳转路径看是不是漏注册或者路径写错了。4.3 webview通信、蓝牙和其他边角功能是否值得做热词里有些问题很有意思这里顺手说一下。“uni-app微信小程序webview如何像H5通信”。如果你在项目里嵌入了web-view组件做活动专题页要和小程序通信记住一个关键点uni.postMessage不是随便就能收到消息的它只在特定时机页面返回、分享、销毁才把消息传给小程序端。所以“H5页内实时同步状态给小程序”这个需求原生的web-view并不好做。简单场景可以用wx.miniProgram.postMessage然后在小程序端的onUnload或onShow里读取事件参数。如果要做复杂的实时通信建议用后端中转别在web-view通信机制上死磕。“uni-app ble ios可以根据蓝牙的deviceid建立连接吗”答案是理论上可以uni.openBluetoothAdapter和uni.createBLEConnection传入deviceId就能连但在iOS上蓝牙权限管理很严格需要在manifest里配置NSBluetoothAlwaysUsageDescription否则系统直接拒绝。这个其实和购物平台本身关系不大但如果你的选题加上了“智能设备配件商城”的方向在商品详情页做个绑定设备的小功能会是很亮眼的扩展点。“charles抓包电脑端微信小程序”这个主要用在后端联调阶段。开发者工具的Network面板能看到全部请求真机调试时需要抓包让手机代理指向电脑的charles同时小程序右上角打开调试模式。如果抓不到明文先检查代理设置再检查小程序是否在调试状态。更省事的方式是在request.js里加日志打印真机调试直接看vConsole。4.4 几个让你心累但答案很简单的“边角问题”“微信小程序右上角三个点和圆圈怎么关闭”——不用关也关不掉那是微信官方胶囊按钮开发者没有权限。如果产品经理让你处理直接告诉他这是系统入口动不了。“微信小程序顶部导航栏高度”前面给过计算代码但补充一点自定义导航栏时胶囊按钮的高度在多数机型上是32px左右状态栏高度通过uni.getSystemInfoSync().statusBarHeight获取。绝对不能把导航栏高度写死成44px一旦遇到大刘海屏标题会顶到状态栏。“微信小程序跳转链接weixin://dl/business”这类scheme主要用于服务号跳转小程序或者业务方拉起另一个小程序需要企业管理员权限和特定资质个人主体基本申请不下来。普通开发者不要碰这类非官方文档里的跳转协议平台一旦调整策略链接就失效了。最后说点实际的体会。市面上购物平台的代码一抓一大把但能不能从“能跑”变成“像样”关键不在用了多少新技术而在边界处理是不是到位。登录态过期、防重复提交、服务端价格校验、订单状态机、分包加载这些不起眼的细节才是答辩或者面试时最能展示项目深度的点。做完这套项目之后建议把它沉淀成自己的通用模板商品、订单、用户这三层骨架是电商类项目复用度最高的部分下次再接到类似需求就能直接起手了。
返回列表