做毕设或者个人练手项目的时候,一说到“购物平台”,十个人里有八个都选微信小程序。原因很简单:微信有流量、有生态,小程序开发门槛又不高,做完还能直接用手机扫一扫演示,比纯Web页面有说服力多了。但如果真动手去做,你会发现从注册AppID、搭项目、写接口,到审核上线,每一步都有坑。“基于uni-app的在线购物平台”这个题目听起来像烂大街的毕设题目,实际上把它做扎实了,含金量并不低。
这期就把我完整做过的这套uni-app购物小程序的全过程拆开聊。内容覆盖技术选型、目录结构、核心页面、后端接口、数据库设计、支付流程、常见报错,不整虚的,都是能直接上手的实操方案。无论你是拿它做毕业设计、课程设计,还是单纯想学小程序开发,这篇文章都能少走不少弯路。
1. 项目整体设计与技术选型思路
1.1 为什么选uni-app而不是原生小程序
选uni-app做跨端购物小程序,核心原因就三个:一套代码多端复用、Vue语法上手快、插件生态省事。原生微信小程序用的是WXML+WXSS+JS那一套,虽然也简单,但写完之后你会发现,同样的业务逻辑换个端就得重写。比如你做完微信小程序,想再出一版支付宝小程序、抖音小程序,原生写法基本是推倒重来。uni-app基于Vue语法,编译时通过条件编译把代码分发到不同平台,业务层只需要维护一套,这个优势在项目后期特别明显。
对于购物类项目来说,uni-app还有一个隐藏优势:它的API封装粒度更接近业务。比如选择收货地址,原生需要调用wx.chooseAddress,换到支付宝端要改成my.chooseAddress,在uni-app里统一成uni.chooseAddress,内部自动做平台转换。这个封装层省掉的不是几行代码,而是跨端适配时的排查时间。
当然,原生小程序也有它的价值,包体更小、运行时有更强的平台能力调优空间,性能敏感型应用可以考虑。但购物平台这种以业务逻辑为主的场景,性能瓶颈在图片、列表、接口响应上,跟用的是不是原生关系不大。uni-app的损耗在可接受范围内,换取的是开发效率和维护成本的大幅降低。
1.2 购物平台的模块边界划分
一个完整的在线购物系统,从功能模块上可以拆成两个端:C端用户小程序和B端管理后台。小程序端面向消费者,核心模块包括首页、分类、购物车、订单、个人中心五大Tab,再往下拆是商品详情、搜索、下单结算、支付、收货地址、售后等二级页面。管理后台面向运营者,负责商品上下架、分类管理、订单处理、库存维护、数据统计。
做毕设时很多人会忽略一件事:管理后台的复杂度往往比小程序端还高。你可以用uni-app再做一套管理端,也可以直接用Vue + Element UI做一套Web管理后台,甚至为了演示方便做成一个内嵌在小程序里的管理员页面。我的建议是尽量前后端分离、管理端独立部署,这样技术难点展示得更充分,答辩或汇报时也更有说头。
两个端共用同一套后端API,通过角色权限做区分。用户在登录时带上身份标识(比如role字段),后端根据角色决定接口返回的数据量级和操作权限。这样设计的好处是数据模型统一,商品、订单、用户这些核心表不需要做双份。
1.3 技术栈选型与版本取舍
- 前端框架:uni-app(Vue 3语法),HBuilderX 3.x及以上版本直接支持
- 状态管理:Vuex(项目不大时Pinia也可以,但uni-app对Pinia的支持目前还不算特别稳)
- UI组件库:uView Plus 或 uni-ui,两者二选一即可,不要混用
- 后端:PHP(ThinkPHP框架)、Node.js(Express/Koa)或 Java(Spring Boot),任选一个你熟悉的。PHP和Node在毕设里最常用,Spring Boot适合写进简历
- 数据库:MySQL 5.7及以上,表结构以订单、商品、用户为核心
- 接口风格:RESTful API,返回JSON数据,登录鉴权用JWT Token
这里有个切身体会:选UI组件库时不要贪多。uView Plus虽然组件全、颜值高,但引入方式要注意,否则会跟uni-app自带的样式起冲突。我一个朋友就是全量引入uView之后,原生button的默认样式被改得乱七八糟,排查了半天。建议按需引入或只引入UI组件,不要全量导入CSS。
2. 前端核心细节与实操要点
2.1 项目初始化与三个核心配置文件的职责
用HBuilderX创建uni-app项目,在“文件 - 新建 - 项目”里选择“uni-app”模板,输入项目名称即可。生成目录后,必须处理好三个文件,这是整个小程序的“地基”。
pages.json是页面路由和窗口样式的总配置文件,相当于原生小程序的app.json。所有页面都要在pages数组里注册,tabBar里配置底部导航。这里有个常被忽略的点:tabBar页面必须放在pages数组的前几项,而且tabBar.list最多只能配置5个,超出会直接编译报错。
manifest.json配置应用名称、AppID、组件库版本。微信小程序AppID在微信公众平台注册后获取,测试时可以先用测试号。特别注意mp-weixin节点下的appid字段,很多人把AppID填到了mp-alipay或其它平台,结果运行到微信时还是提示AppID无效。
App.vue是整个应用的根组件,应用生命周期onLaunch在这里触发,适合做全局登录检查、检查更新、初始化全局数据等操作。但要注意App.vue里的onLaunch在冷启动时执行,异步请求不要写得太重,否则会影响首页首屏渲染速度。
2.2 顶部导航栏与iPhone刘海屏适配
“微信小程序顶部导航栏高度”这个搜索词的点击量一直不低,就是因为自定义导航时,不同机型的状态栏高度不一致,导致导航栏布局错乱。
默认导航栏用原生渲染,不需要考虑适配。但很多购物平台为了视觉统一,会选择自定义导航栏:把pages.json里页面的navigationStyle设为custom,然后自己在页面顶部画一个导航条。
自定义导航时,状态栏高度需要动态获取:
// utils/system.js export function getStatusBarHeight() { // 注意:uni.getSystemInfoSync是同步方法,可以安全在onLoad里调用 const systemInfo = uni.getSystemInfoSync() return systemInfo.statusBarHeight || 20 } export function getMenuButtonBoundingClientRect() { // 胶囊按钮位置,只在微信小程序中有效 // #ifdef MP-WEIXIN const menuButtonInfo = uni.getMenuButtonBoundingClientRect() return menuButtonInfo // #endif // 非微信环境给个兜底值 return { top: 20, height: 30 } }拿到状态栏高度和胶囊按钮位置后,导航栏高度 = 状态栏高度 + 胶囊按钮高度 + 胶囊按钮上下间距 × 2。这个公式在自定义导航时通用,能精确计算出导航栏的实际高度,避免内容被刘海遮挡或导航栏偏矮。
2.3 请求封装与登录态管理
购物平台的每个用户操作基本都要带登录态,所以请求封装是第一优先级。我的做法是在utils/request.js里封装一个基于Promise的request函数,统一处理baseURL、请求头、Token注入、状态码拦截、错误提示。
// utils/request.js const BASE_URL = 'https://你的服务器域名.com' export function request({ url, method = 'GET', data = {}, needAuth = true }) { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token') const header = { 'Content-Type': 'application/json' } if (needAuth && token) header['Authorization'] = `Bearer ${token}` uni.request({ url: BASE_URL + url, method, data, header, success: (res) => { if (res.statusCode === 401) { // Token过期或无效,清除本地登录态并跳转登录 uni.removeStorageSync('token') uni.removeStorageSync('userInfo') uni.navigateTo({ url: '/pages/login/login' }) return } if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data) } else { uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }登录流程走的是微信登录:小程序端调用uni.login拿临时code,把code发给后端,后端拿着code向微信接口换取openid和session_key,再签发自己的JWT Token返回给前端。前端把Token存入Storage,后续请求自动带上。这里有一个非常容易踩的坑:code只能用一次,换完openid就失效,所以不能让前端反复提交同一个code。调试时遇到“40029 code无效”报错,八成是这里的问题。
2.4 商品列表与购物车状态管理
购物车是典型的跨页面共享数据场景。首页点“加入购物车”,购物车页面要立即显示最新数量和金额,中间如果只靠onShow重新拉接口,体验会慢半拍,而且还要处理并发。用Vuex管理购物车数据是最合理的方案。
我在store/modules/cart.js里维护购物车列表、选中状态、总金额三个核心状态:
const state = { cartList: [], // [{ goodsId, goodsName, price, count, checked }] checkedGoods: [], // 选中的商品项 } const getters = { cartTotal(state) { return state.cartList.reduce((total, item) => total + item.count, 0) }, checkedTotalPrice(state) { return state.cartList .filter(item => item.checked) .reduce((total, item) => total + item.price * item.count, 0) } }所有修改购物车的操作通过mutations同步修改,比如ADD_TO_CART、TOGGLE_CHECKED、UPDATE_COUNT、DELETE_ITEM。组件内部computed里用mapGetters映射cartTotal和checkedTotalPrice,这样不管在哪个页面修改了购物车,所有引用的页面都会自动更新。
这个方案有一个好处:用户从购物车进结算页时,不需要再次请求接口,直接用内存里的数据就能拼出订单预览,速度快、体验好。缺点也很明显:如果用户同时在多端操作购物车,本地状态会和服务端不一致。解决办法是加购、删购时同步调接口,但页面展示以本地Vuex为主,以后端返回为准,形成“本地优先、后端兜底”的同步策略。
2.5 全局弹窗组件封装思路
官方uni.showToast确实只支持文本和icon,自定义程度低。如果想让弹窗支持图片、按钮、自定义插槽,最简单的方案是自己封装一个全局弹窗组件。
我之前写过一个轻量的customToast组件,核心思路是:在components目录里建一个popup-dialog组件,组件内部用uni.showModal的样式做基础,但内容用slot自定义。再用一个全局的uni.$emit事件机制触发弹窗。
<template> <view v-if="visible" class="popup-wrapper"> <view class="mask" @click="onClose"></view> <view class="popup-content"> <slot name="content"></slot> <view class="btn-group"> <button @click="onCancel">取消</button> <button type="primary" @click="onConfirm">确定</button> </view> </view> </view> </template> <script> export default { name: 'CustomPopup', data() { return { visible: false } }, methods: { open(config) { this.visible = true // config里可以传 title, content, confirmText等 }, close() { this.visible = false } } } </script>封装好了在需要弹窗的页面里引入一次,通过uni.$emit('open-popup', {...})即可唤起。这样比每个页面单独写一套弹窗逻辑省事得多,样式也统一。需要注意:弹窗组件的visible状态要记得在页面卸载时重置,否则从A页跳到B页再返回,弹窗可能还在。我就是踩过一次这个坑,后来在onUnload里补了一个close处理。
3. 后端接口设计与数据库实现
3.1 数据库核心表结构设计
购物平台的数据表设计是整个系统的重心。表结构设计得合理,后面写接口、做统计都会很顺畅;设计不合理,订单和库存对不上、退款状态混乱,是你后期debug最痛苦的事情。
我的核心表一共7张:用户表、商品表、商品分类表、购物车表、订单表、订单商品表、收货地址表。订单表与订单商品表分离是为了支持一个订单包含多个商品(多商品合并结算场景),如果一张订单只对应一个商品,那订单商品表可以直接并入订单表。
用户表 user
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 主键,自增 |
| openid | varchar(64) | 微信openid,唯一索引 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像URL |
| phone | varchar(20) | 手机号(选填) |
| role | tinyint | 角色:0普通用户,1管理员 |
| create_time | datetime | 注册时间 |
商品表 goods
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| category_id | int | 分类ID,关联分类表 |
| title | varchar(100) | 商品标题 |
| subtitle | varchar(200) | 商品副标题 |
| main_image | varchar(255) | 主图URL |
| images | text | 商品轮播图,JSON数组存储 |
| price | decimal(10,2) | 售价 |
| original_price | decimal(10,2) | 原价(划线价) |
| stock | int | 库存 |
| sales | int | 销量 |
| detail | text | 富文本或图文详情 |
| status | tinyint | 状态:0下架,1上架 |
| create_time | datetime | 创建时间 |
订单表 order
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| order_no | varchar(32) | 订单号,唯一索引 |
| user_id | int | 用户ID |
| total_price | decimal(10,2) | 订单总金额 |
| pay_price | decimal(10,2) | 实付金额 |
| pay_type | tinyint | 支付方式:1微信支付 |
| status | tinyint | 订单状态:见下方状态机 |
| address_id | int | 收货地址ID |
| transaction_id | varchar(64) | 微信支付交易号 |
| remark | varchar(255) | 用户备注 |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 支付时间 |
| ship_time | datetime | 发货时间 |
| finish_time | datetime | 完成时间 |
订单号生成建议用date('YmdHis') + rand(1000,9999)这种格式,或者直接用年月日时分秒+用户ID后四位,保证可读性和唯一性。不要用自增ID直接当订单号,会被用户猜到订单规模。
3.2 接口清单与RESTful规范
接口按模块划分,统一返回格式为{ code, msg, data },成功时code为0,失败时为错误码。RESTful风格上,列表用GET,新增用POST,更新用PUT/PATCH,删除用DELETE。
用户模块
POST /api/auth/login微信登录,参数code,返回token和用户信息GET /api/user/info获取用户信息,请求头带TokenPUT /api/user/info更新用户资料
商品模块
GET /api/goods/list商品列表,参数:keyword、categoryId、page、pageSize、sortByGET /api/goods/detail商品详情,参数:goodsIdGET /api/category/list分类列表
购物车模块
GET /api/cart/list获取购物车列表POST /api/cart/add加入购物车,参数:goodsId、countPUT /api/cart/update修改购物车数量或选中状态DELETE /api/cart/remove删除购物车条目
订单模块
POST /api/order/create创建订单,参数:addressId、goodsList、remarkPOST /api/order/pay发起支付,参数:orderNoGET /api/order/list订单列表,参数:status(0全部,1待付款等)GET /api/order/detail订单详情,参数:orderNoPOST /api/order/cancel取消订单POST /api/order/confirm确认收货
接口的鉴权策略很简单:除登录和商品列表、详情外,其他接口都要求请求头携带Authorization: Bearer token。后端在Controller里写一个基础类的checkAuth方法,每个需要鉴权的接口先调这个方法,减少重复代码。
3.3 后端登录接口实现(PHP示例)
用PHP写登录接口,思路很直观。拿前端的code,调微信官方接口换openid,然后查库决定是登录还是自动注册。
// Api/AuthController.php public function login(Request $request) { $code = $request->post('code'); $appid = '你的小程序AppID'; $secret = '你的小程序AppSecret'; // 向微信服务器换取 openid $url = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code"; $result = file_get_contents($url); $result = json_decode($result, true); if (isset($result['errcode'])) { return json(['code' => 500, 'msg' => '登录失败']); } $openid = $result['openid']; // 查询用户,不存在则自动注册 $user = Db::name('user')->where('openid', $openid)->find(); if (!$user) { $userId = Db::name('user')->insertGetId([ 'openid' => $openid, 'nickname' => '微信用户' . mt_rand(10000, 99999), 'create_time' => date('Y-m-d H:i:s') ]); } else { $userId = $user['id']; } // 生成Token(这里用简单的md5拼接,生产环境建议用JWT) $token = md5($openid . time() . mt_rand(1000, 9999)); Db::name('user_token')->insert([ 'user_id' => $userId, 'token' => $token, 'expire_time' => date('Y-m-d H:i:s', time() + 7200) ]); return json(['code' => 0, 'data' => ['token' => $token, 'userInfo' => $user]]); }注意:file_get_contents拉远程接口在开发环境没问题,但生产环境有的服务器禁用了allow_url_fopen,就调用失败。更稳妥的方式是用cURL封装,或者用ThinkPHP自带的Http类。另外一个调试技巧:微信的jscode2session接口在开发者工具里能正常返回,但真机预览时报invalid code,很可能是AppID配错了,检查manifest.json里是不是填的AppID和后端secret对应的是同一个小程序。
3.4 订单状态机设计
订单状态是购物平台最容易写乱的模块。我建议把状态定义写在接口文档和维护手册的第一页,前后端统一下来。
| 状态值 | 状态含义 | 前端按钮展示 | 用户可执行操作 |
|---|---|---|---|
| 0 | 待付款 | 去支付、取消订单 | 继续支付 / 关闭订单 |
| 1 | 待发货 | 提醒发货 | 申请退款 |
| 2 | 待收货 | 查看物流、确认收货 | 确认收货 |
| 3 | 待评价 | 去评价 | 发表评价 |
| 4 | 已完成 | 查看详情 | 再次购买 |
| 5 | 已关闭 | 删除订单 | 删除 |
| 6 | 退款中 | 查看进度 | 撤销申请 |
这里有个容易漏掉的状态:用户付款后、商家发货前,订单是“待发货”状态;商家发货后、用户确认收货前,是“待收货”状态。两者在用户侧看起来都是已付款,但后端逻辑不同:待发货时用户可申请退款,待发货状态商家可发货。我在做第一次版本时把这两个状态合并了,导致用户付款后直接看到“确认收货”按钮,点下去商家还没发货订单就完成了,场面一度很尴尬。后来才把状态细分建模。
4. 实操过程:从零搭建购物平台的完整流程
4.1 HBuilderX创建项目并运行到微信开发者工具
首次用HBuilderX开发微信小程序,需要先把运行环境配置好,关键两步:微信开发者工具的安全设置里开启“服务端口”,HBuilderX才能自动拉起小程序。
- 打开HBuilderX,新建项目,选择“uni-app”模板
- 在
manifest.json的mp-weixin节点填入微信小程序的AppID - 菜单栏“运行 - 运行到小程序模拟器 - 微信开发者工具”
- 首次运行会在微信开发者工具中自动打开项目,后续代码保存会自动同步编译
这个阶段经常遇到的报错是“HBuilderX连接微信开发者工具失败”,原因基本是微信开发者工具没有开启服务端口。解决路径:微信开发者工具 → 设置 → 安全设置 → 打开“服务端口”。另一个报错是“appid为空”,检查manifest.json里的配置是否正确,测试阶段可以填测试号AppID,但要注意测试号的接口权限有限,wx.requestPayment这类支付接口没法完全测试。
4.2 首页与商品详情页的实现要点
首页是商品的门面,一般由“搜索框 + 轮播图 + 分类金刚区 + 商品瀑布流”组成。轮播图和金刚区都是静态配置数据,可以从后端接口拉取,也可以直接在store里写死,看项目需要。商品瀑布流用scroll-view配合触底加载分页,每页拉10条或20条,性能更好。
商品列表的分页是高频考点。后端接口接收page和pageSize,返回total、list、hasMore三个字段。前端用onReachBottom触发加载下一页,判断hasMore为false时不再请求:
// pages/home/home.vue data() { return { goodsList: [], page: 1, pageSize: 10, hasMore: true, loading: false } }, methods: { async loadGoods(isRefresh = false) { if (this.loading) return this.loading = true const res = await request({ url: '/api/goods/list', data: { page: this.page, pageSize: this.pageSize } }) if (isRefresh) this.goodsList = [] this.goodsList = this.goodsList.concat(res.data.list) this.hasMore = res.data.list.length === this.pageSize this.page += 1 this.loading = false } }, onReachBottom() { if (this.hasMore) this.loadGoods() }商品详情页主要是展示和操作:轮播图用swiper组件,商品价格、库存、销量是静态渲染,参数规格的展示可以先用纯文本列表,加购按钮触发Vuex的ADD_TO_CARTmutation,同时调后端购物车接口同步。
这里有一个非常实用的交互细节:加购成功后在按钮位置上弹一个小气泡动画,体验提升明显。uni-app用uni.createAnimation就能实现,不用引入额外库。
4.3 购物车页面的全选、单选、金额计算与删除
购物车页面的核心是数据联动:单选/全选影响总金额,数量增减影响总金额,删除影响列表和总金额。这些都在Vuex里维护,组件内部只需要调用mutation和getter。
全选逻辑:
// store/modules/cart.js setAllChecked(state, checked) { state.cartList.forEach(item => { item.checked = checked }) }计算总金额:
const getters = { totalPrice(state) { return state.cartList .filter(item => item.checked) .reduce((sum, item) => sum + item.price * item.count, 0) .toFixed(2) } }删除时要注意和本地缓存的同步。如果购物车数据同时存在本地和Server端,删除时先乐观更新本地Vuex,再向Server发起删除请求,失败时回滚。这种“乐观UI”策略能让界面响应更快,但必须在失败时做好提示和回滚,否则会出现“删了又出现”的笑话。
购物车页面还有一个隐藏需求:空状态。数据为空时要展示一个可爱的空车提示和一键去逛逛的按钮。这个状态是很多新手容易忽略的,实际上线后用户很常见。
4.4 下单与微信支付流程
下单的流程是:购物车选中商品 → 进入确认订单页 → 选择收货地址 → 填写备注 → 点击提交 → 创建订单 → 发起支付 → 支付成功 → 回到订单列表。
微信支付在当前项目里的具体实现:
- 后端调用微信支付统一下单接口,拿到
prepay_id - 后端根据
prepay_id生成支付参数(timeStamp、nonceStr、package、signType、paySign),返回给前端 - 前端调用
uni.requestPayment,传入支付参数 - 用户在微信内完成支付,微信服务器把支付结果回调到后端配置的回调URL
// pages/order/confirm.vue 核心支付代码 const res = await request({ url: '/api/order/pay', method: 'POST', data: { orderNo: this.orderNo } }) if (res.code === 0) { const payParams = res.data uni.requestPayment({ provider: 'wxpay', timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: 'MD5', paySign: payParams.paySign, success: (res2) => { uni.showToast({ title: '支付成功', icon: 'success' }) setTimeout(() => { uni.redirectTo({ url: '/pages/order/list?status=1' }) }, 1500) }, fail: (err) => { uni.showToast({ title: '支付取消', icon: 'none' }) } }) }支付这块有个大坑:个人主体的小程序无法开通微信支付,必须是企业主体、个体工商户主体才能申请。如果只是做毕设演示,可以用“模拟支付”——前端点支付时直接调一个/api/order/mockPay接口,后端把订单状态改成“待发货”。答辩时说明这是演示环境的模拟支付,正式环境接入微信支付即可。千万不要在毕设演示时真去用一个没有资质的小程序调支付,审核过不了,演示也尴尬。
4.5 订单列表与个人中心
订单列表按状态筛选:全部、待付款、待发货、待收货、待评价。每个tab对应一个订单状态值,界面用scroll-view做横向tab滚动,列表本身用onReachBottom做分页加载。
个人中心需要展示用户头像、昵称、订单入口、收货地址入口、售后入口。数据来源有两个:本地Storage里的用户信息缓存 + 后端/api/user/info接口。头像和昵称在微信小程序里获取有改版,从wx.getUserProfile改成头像昵称填写能力,很多老教程还在用旧API,照抄会拿不到数据。
实际操作里,个人中心的头像可以用button的open-type="chooseAvatar"实现,昵称用input的type="nickname"实现,这是目前微信官方推荐的做法。
5. 常见问题与排查技巧实录
5.1 request:fail url not in domain list
这是新手最容易遇到的报错,意思是请求的域名没有在小程序后台配置为合法域名。解决方法是到微信公众平台 → 开发管理 → 开发设置 → 服务器域名里,把接口域名配置到request合法域名里。
开发阶段有个临时办法:在微信开发者工具里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”,这样本地调试可以绕过域名校验。但注意,这个选项只对开发者工具生效,真机预览时必须配好合法域名,否则接口全部失败。
另一个容易忽略的点:开发阶段用http://localhost或者http://192.168.x.x调试接口,换真机预览时发现连不上,因为你的手机和电脑不在同一个局域网,或者防火墙没放行端口。解决办法是让手机和电脑连同一个WiFi,用电脑的局域网IP作为接口地址。
5.2 安全区域与底部小黑条适配
iPhone从X开始有底部小黑条(Home Indicator),页面底部如果有“提交订单”“立即支付”这类吸底按钮,会被小黑条遮住一部分。解决办法是给吸底按钮容器加上安全区适配:
.safe-bottom { padding-bottom: constant(safe-area-inset-bottom); /* iOS 11.0 */ padding-bottom: env(safe-area-inset-bottom); /* iOS 11.2+ */ }uni-app里更简单的方案是在pages.json的app-plus节点下配置safearea,或者在页面上直接用uni.getSystemInfoSync()拿到safeAreaInsets,动态计算底部高度。
5.3 商品数据量大的列表渲染优化
购物平台商品列表动辄几百上千条,一次性渲染会卡顿。常规优化手段有3个:
- 分页加载,控制每次渲染10-20条
- 图片懒加载,
image组件加lazy-load属性 - 使用
uni.createSelectorQuery监听滚动到指定位置时再渲染可视区内容(类似虚拟列表)
分页是首要手段,简单有效。图片懒加载是默认必须开的。虚拟列表在数据量超过100条时再考虑,毕设级别用分页就够。
5.4 原生组件层级遮挡问题
微信小程序里,canvas、video、map这类原生组件层级最高,会覆盖普通view组件。购物平台里如果商品详情页用了video,底部的加购栏会被视频盖住。旧方案的解决办法是用cover-view包裹底部按钮,或者把视频封装进一个容器里设置同层渲染属性。新版本微信基础库已经默认同层渲染,但遇到层级问题时要第一时间想到是原生组件在捣鬼。
5.5 Token过期与登录态丢失
Token的过期策略一般是2小时或7天。过期后,请求会返回401。前端的处理方式是:清掉本地Token和用户信息,跳登录页,同时给用户一个toast提示“登录已过期,请重新登录”。
如果用户正在填写购物车,跳走之后数据会丢。更人性化的做法是:弹出一个Modal,提示登录过期,给用户两个按钮:“重新登录”和“取消”,取消则停留在当前页面,重新登录成功后回到原页面。这个功能的实现不复杂,但很加分,答辩时可以提一下。
5.6 真机调试时白屏
白屏是最让人头大的问题。我遇到过的原因按概率排序:
- 接口域名没配好,请求全部失败
- 页面JS报错,错误被吞了(在
onLoad里异步抛错,工具不提示) - 使用了不兼容的CSS属性,在某些安卓机型上渲染异常
- 内存不足,图片太多导致webview崩溃
排查白屏的第一步:看微信开发者工具的Console面板,有没有红字报错。没有报错的话,逐步注释页面代码定位问题区域。这个方法虽然土,但极其有效。真机白屏还有一个杀手锏:关掉“自动预览”,用“真机调试”模式,能看到真机上的Console日志。
6. 项目扩展方向
购物平台做完基础版之后,其实可扩展的方向很多。我建议按优先级考虑:SKU多维规格(颜色、尺码)、优惠券系统、秒杀活动、会员积分、售后申请、商品评价体系。这些功能在电商场景里都是闭环的,加一个功能就能让项目的完整度和亮点上一个台阶。
如果做毕业设计,我推荐加“优惠券”和“商品评价”,因为这俩在答辩时最好讲:优惠券涉及库存、有效期、满减规则这些业务逻辑,评价涉及图片上传和审核状态,都有清晰的技术难点可以展开。
7. 个人经验与实操感受
最后分享几个最实际的体会。
组件库不要贪多,平时用原生view写写样式也是一种锻炼。我认识很多人一开始就引入全套uView,写起来舒服了,但对底层组件的工作原理其实很模糊,一旦遇到自定义样式需求就卡壳。建议从简单页面纯手写样式开始,UI库只用在复杂组件场景。
调试阶段多用console.log,少猜。我见过太多人报错不先看控制台,而是直接查“xx报错怎么办”,其实很多问题看第一行报错信息就能知道答案。console.log调试法虽然原始,但在小程序开发里依然是最可靠的定位手段。
项目做完之后,花一天时间把所有接口和页面过一遍,把兼容性问题、状态转换关系整理成文档。这份东西毕业设计答辩时是加分项,工作面试时也能证明你有整理和复盘的习惯。如果你还打算继续优化,建议从订单模块的并发处理入手,这是购物平台最容易出技术深度的地方。
做这个项目的过程中,我最深的感受是:看似普通的购物平台,涉及的要点一点不少,从跨端框架到微信生态限制,从数据状态管理到支付回调联动,每一个环节都在逼你“知其然,还知其所以然”。把这个项目真正跑通,你学到的不仅是小程序开发,更是完整的业务系统设计思路。