微信小程序商城demo实战:从购物车到订单闭环实现

微信小程序商城demo实战:从购物车到订单闭环实现 简介本资源是一套完整的微信小程序商城功能演示项目面向小程序初学者与前端开发入门者旨在帮助开发者快速理解商城类应用的核心页面结构与交互逻辑。压缩包共57个文件包含12个样式文件wxss、11个逻辑脚本js、10个视图模板wxml、5个配置文件json及15张界面截图png辅以字体文件与SVG图标完整覆盖首页、商品列表、详情页、购物车及个人中心等典型模块总大小仅95KB轻量易导入调试。已有5994人下载学习适合用于课堂实训、自学练手或项目参考。资源提供可直接运行的源码结构目录清晰含app.js/app.json/app.wxss及pages/、utils/、static/等标准路径并附带真实界面截图便于效果对照有效降低新手在路由配置、数据绑定与组件复用等环节的理解门槛。 最近整理了一个完整的微信小程序商城demo带源代码和截图正好趁这个机会把整个实现思路和踩坑过程梳理一遍。这个项目就是一个典型的小商城首页商品列表、商品详情、购物车、订单提交基本覆盖了电商类小程序最常见的闭环。如果你刚学完小程序基础、想找个练手项目或者需要一套能快速改造成自己风格的模板这份笔记应该能帮你省不少时间。我尽量把每个步骤为什么这么做讲清楚代码也放出来方便对照参考。1. 项目整体设计与技术选型1.1 商城demo的需求拆解动手写代码之前先把功能边界划清楚。一个小程序商城demo和真实商城不一样真实商城要考虑会员体系、营销工具、客服消息、支付回调、售后流程但这些对一个学习型的demo来说太重了。我这个demo定成最基础的一条购买链路用户进来看商品点进详情加入购物车去购物车确认数量提交订单。够用又不至于让代码膨胀到劝退新手。具体功能划分成这几个模块首页商品列表展示商品图片、名称、价格支持点击进入详情。商品详情页展示更完整信息有“加入购物车”按钮。购物车页面列表展示已加入的商品支持数量增减、删除、单选全选自动计算总金额。订单确认页从购物车中选中商品生成订单展示订单金额和收货信息demo里是写死的。订单列表页展示已经提交的订单方便查看流程闭环。没有做搜索、分类、个人中心原因是这些模块属于锦上添花并不影响核心交易链路。如果后面有时间把搜索和分类加上也不难就是多两个列表页的事。先把核心链路跑通再考虑扩展这是我觉得做demo最务实的节奏。1.2 为什么选原生小程序而不是uni-app或Taro很多人一上来就喜欢问“用uni-app行不行”我的回答是行但这次的demo我特意选了原生微信小程序。原因有三个。第一原生语法就是小程序的基础WXML、WXSS、JavaScript、JSON四件套所有跨端框架最后都要编译成这些东西。你要是连原生组件生命周期都没摸过直接上框架遇到问题会分不清是框架的问题还是小程序自身的问题。第二原生项目体积小打开微信开发者工具就能跑不需要额外安装Node依赖虽然装了也没事对新手更友好。第三这个demo规模不大页面才五六个用跨端框架的优势体现不出来反而增加一层概念负担。当然如果你以后接的项目需要同时上支付宝小程序、抖音小程序uni-app确实值得学。但那是工程化需求不是demo需求。想快速出效果、学习小程序底层能力原生永远是最直接的路径。1.3 数据方案本地mock数据与云开发的取舍商城必然要有商品数据没有后端怎么办我试过几种方式最简单的就是在项目里放一个utils/mock.js用数组模拟数据库页面直接require进来用。// utils/mock.js const goodsList [ { id: 1, title: 白色纯棉T恤, price: 79, image: /images/t-shirt.png, desc: 100%纯棉透气舒适基础百搭款。, stock: 100 }, { id: 2, title: 轻便双肩包, price: 199, image: /images/bag.png, desc: 防泼水尼龙面料可容纳14寸笔记本。, stock: 64 }, // 继续添加... ]; module.exports { goodsList };这种方式的优点非常明显不用配服务器、不用管域名备案、不需要考虑token鉴权下载代码就能跑特别适合学习。缺点也明显比如刷新页面数据不会变、没有办法模拟登录态、不能验证支付回调等但这对demo来说完全够了。如果你希望更“真实”一点也可以直接上微信云开发。云开发自带云函数、云数据库和云存储天然免鉴权、免域名对小项目很友好。我在后续扩展时会把商品数据挪到云数据库再用云函数做接口。但这里还是先说本地mock因为一个demo首先要保证的是“跑起来”而不是“架构先进”。1.4 项目目录结构与app.json全局配置先看目录结构。我做项目习惯把页面、公共样式、工具类、图片分离这样后期不会乱。miniprogram/ ├─ app.js ├─ app.json ├─ app.wxss ├─ utils/ │ └─ mock.js ├─ images/ │ ├─ t-shirt.png │ ├─ bag.png │ └─ cart.png └─ pages/ ├─ index/ │ ├─ index.js │ ├─ index.json │ ├─ index.wxml │ └─ index.wxss ├─ detail/ ├─ cart/ ├─ order/ └─ orders/如果项目是用微信开发者工具新建的可能默认有miniprogram和cloudfunctions两个根目录。我们不用云开发直接忽略cloudfunctions目录把页面放在miniprogram/pages下就对了app.json在miniprogram目录内。app.json是小程序的全局配置底部tabBar、页面路由都是在这里声明的。我配置了三个tab首页、购物车、订单列表。截图里默认展示首页方便一眼看出整个项目是什么类型。{ pages: [ pages/index/index, pages/detail/detail, pages/cart/cart, pages/order/order, pages/orders/orders ], window: { navigationBarTitleText: 商城demo, navigationBarBackgroundColor: #ffffff, navigationBarTextStyle: black, backgroundColor: #f5f5f5 }, tabBar: { color: #999999, selectedColor: #ff4d4f, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/cart/cart, text: 购物车 }, { pagePath: pages/orders/orders, text: 订单 } ] }, style: v2, sitemapLocation: sitemap.json }注意tabBar.list最少需要2项最多5项我放了3个。如果你不想在tabBar里用图片直接只写“text”也是可以的不过有图标会好看一些。detail和order这两个页面不需要出现在tabBar里所以只放在pages数组中通过wx.navigateTo跳转。2. 核心功能实现从首页到购物车2.1 首页商品列表的实现首页是所有商城项目的门面我用了最简单的列表布局上下滚动每个商品平铺展示包含图片、标题、价格。核心逻辑就是onLoad里读取mock数据setData后渲染WXML。// pages/index/index.js const { goodsList } require(../../utils/mock.js); Page({ data: { goodsList: [] }, onLoad() { this.setData({ goodsList }); }, goDetail(e) { const id e.currentTarget.dataset.id; wx.navigateTo({ url: /pages/detail/detail?id${id} }); } });这里有两个小细节值得说。第一个是e.currentTarget.dataset.id的取值方式。WXML里我们在view上写>!-- pages/index/index.wxml -- view classgoods-list view classgoods-item wx:for{{goodsList}} wx:keyid >// pages/detail/detail.js const { goodsList } require(../../utils/mock.js); Page({ data: { goods: null, count: 1 }, onLoad(options) { const id Number(options.id); const goods goodsList.find(item item.id id); this.setData({ goods }); }, addToCart() { const { goods, count } this.data; if (!goods) return; const cart wx.getStorageSync(cart) || []; const idx cart.findIndex(item item.id goods.id); if (idx -1) { cart[idx].count count; } else { cart.push({ id: goods.id, count }); } wx.setStorageSync(cart, cart); wx.showToast({ title: 已加入购物车, icon: success }); } });购物车的数据我选择存在本地缓存wx.setStorageSync里这样即使杀掉小程序再打开购物车数据还在。这个方案放在真实环境里也说得通因为购物车本质上就是“本地行为服务端同步”很多轻量App都会先把用户操作缓存到本地等合适的时机再上报。这里要说明一个细节购物车里存的对象只保存了商品id和数量没有存商品标题、价格、图片。这是刻意做的。为什么因为价格和标题是商品本身的属性应该以服务端数据为准。如果购物车里冗余了价格后来商品价格变动了购物车就会显示旧价格。所以我在购物车页面渲染时会拿id去mock数据里匹配商品信息来显示。这种方式虽然多一步查找但保证了数据一致性也更能模拟真实场景。详情页的WXML我就不贴完整代码了核心就是展示一张大图、标题、价格、描述文本底部固定一个“加入购物车”按钮。按钮的样式建议用position: fixed; bottom: 0固定在底部这是电商详情页的标准交互。2.3 购物车页面的数据联动购物车页面是整个demo里逻辑最绕、但也是最值得认真看的地方。它需要读取本地缓存展示商品列表还要处理数量增减、全选、单选、删除、总价计算任何一个联动出问题都会直接影响用户体验。我把它拆成了几个方法getCartList负责从缓存中读取并组装完整数据changeCount处理数量变化toggleSelect处理单选toggleSelectAll处理全选deleteItem删除商品getTotalPrice计算选中商品总额。// pages/cart/cart.js const { goodsList } require(../../utils/mock.js); Page({ data: { cartList: [], selectAll: false, totalPrice: 0 }, onShow() { this.getCartList(); }, getCartList() { const cart wx.getStorageSync(cart) || []; const cartList cart.map(item { const goods goodsList.find(g g.id item.id); return { ...goods, count: item.count, selected: true }; }).filter(item item ! undefined); this.setData({ cartList }); this.getTotalPrice(); }, getTotalPrice() { const cartList this.data.cartList; const total cartList .filter(item item.selected) .reduce((sum, item) sum item.price * item.count, 0); this.setData({ totalPrice: total }); } });在onShow里调用getCartList很关键。为什么不用onLoad因为页面从详情页跳转过来时如果页面栈里已经存在cart页面onLoad不会重新执行但onShow每次显示都会触发。这样可以保证用户从详情页加购后点tab切回购物车看到的永远是最新数据。数量加减是一个数组更新操作微信小程序不像Vue那样可以直接改数组索引触发响应式。必须通过setData更新整个数组或者用this.setData({ [cartList[0].count]: 2 })这种方式。我习惯创建新数组再整体更新代码更直观changeCount(e) { const { index, type } e.currentTarget.dataset; const cartList this.data.cartList; const item cartList[index]; if (type minus) { if (item.count 1) return; item.count--; } else if (type plus) { if (item.count item.stock) { wx.showToast({ title: 库存不足, icon: none }); return; } item.count; } this.setData({ cartList }); // 同步更新本地缓存 const cart wx.getStorageSync(cart) || []; const target cart.find(g g.id item.id); if (target) target.count item.count; wx.setStorageSync(cart, cart); this.getTotalPrice(); }库存判断这里我特意写上了。如果没有这个判断用户把数量加到超过库存后续下单就会出现超卖问题。虽然demo简单但这种边界条件如果能早点考虑后面接真实接口时会省很多事。2.4 订单提交与订单列表订单页面是我整个demo的收口用户从购物车选中商品点击“去结算”跳到订单确认页页面展示商品明细和总金额。因为demo没有支付功能我直接把“提交订单”按钮定义成写入订单库并跳转到订单列表。订单数据结构设计成{ id: Date.now(), createTime: 2025-01-01 10:00:00, goodsList: [ { title: 白色纯棉T恤, price: 79, count: 1 } ], totalPrice: 79, status: 0 }status用数字表示订单状态0代表待支付1代表已支付2代表已完成。真实项目里状态机要复杂得多但demo里用数字足够。提交订单后同时要做两件事把新订单写入缓存把购物车中已下单的商品移除。移除时要注意不能简单清空购物车因为用户可能只选中了部分商品去结算。我用了过滤逻辑把购物车中所有已选中的商品id过滤掉。const { cartList } this.data; const selectedIds cartList.filter(item item.selected).map(item item.id); const cart wx.getStorageSync(cart) || []; const newCart cart.filter(item !selectedIds.includes(item.id)); wx.setStorageSync(cart, newCart);订单列表页就简单了从orders缓存中读取数据循环渲染。这里我用了onShow这样每次从订单确认页返回时都能看到新增的订单。到这里一条完整的购物闭环就走完了。从用户角度看这个demo已经“能用了”。虽然缺少支付、登录等环节但核心数据流已经打通这也是我把它作为练手项目推荐的原因——麻雀虽小五脏俱全。3. 调试配置与常见问题排查3.1 tabBar与顶部导航栏高度适配有不少人私信问过顶部导航栏高度怎么获取尤其是在做自定义导航时。我这次没有自定义导航用的是微信原生导航所以不用处理“状态栏高度导航栏高度”的适配问题。但如果你想让详情页的导航栏透明化、背景渗透进去就必须处理了。获取导航栏高度有两步。第一步用wx.getWindowInfo()基础库2.20.1以上旧版本用wx.getSystemInfoSync()获取statusBarHeight也就是状态栏高度。第二步如果启用了自定义导航navigationBarHeight没有直接的API但可以通过菜单按钮的boundingClientRect计算const windowInfo wx.getWindowInfo(); const menuInfo wx.getMenuButtonBoundingClientRect(); const navBarHeight (menuInfo.top - windowInfo.statusBarHeight) * 2 menuInfo.height;这段逻辑我一般放在app.js里算完后存到全局对象页面里直接取用。注意不同手机的胶囊位置不同动态计算比写死44px要稳妥得多。另外购物车和详情页的底部按钮会被iPhone X系列手机的小黑条“home indicator”遮挡解决方法是给按钮底部加安全区距离.bottom-btn { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }这个在开发者工具里可以模拟iPhone X效果真机上更明显。3.2 request请求域名与本地接口调试很多人在做小程序时绕不开wx.request。这个demo虽然用的是本地mock不用发请求但你还是会碰到这类问题尤其是准备把demo接上真实后端的时候。微信小程序对网络请求有严格的域名限制wx.request的url必须在小程序管理后台配置的合法域名列表里而且必须HTTPS。如果你是在开发调试阶段可以临时在微信开发者工具右上角“详情-本地设置”里勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”。这个选项只对开发工具和本地真机调试有效上线前必须改为正规域名。如果你是用局域网IP访问本地后端比如http://192.168.1.100:3000/api/goods开发工具里开启不校验是可以通的但真机预览时也会受限制。真机调试的话手机和电脑必须在同一局域网并且也要勾选不校验域名选项。这个坑我踩过很多次最后干脆在mock数据阶段就屏蔽掉了网络因素专心调业务逻辑。另外wx.request的请求参数和响应数据都是字符串或ArrayBuffer如果你习惯axios的响应拦截器可能需要自己封装一层。我通常会封装一个request函数统一处理baseURL、token、错误码这样后续业务代码就清爽很多。3.3 图片加载失败与白屏问题商城demo离不开图片。有些人用网络图片链接在开发工具中能显示真机预览时却一片空白。原因大概率是图片域名没被配置到downloadFile合法域名中或者图片是HTTP协议。更稳妥的做法是直接用本地图片把图片放到images目录路径使用绝对路径以/开头比如/images/t-shirt.png。注意WXSS里不能直接使用本地图片背景图加绝对路径其实可以background-image是支持本地图片的但路径要相对于WXSS文件比较容易出错。我建议在WXML中用image组件并且通过modeaspectFill控制缩放这样最省心。如果你要展示“截图”中的效果建议把商品图片换成高清无版权图片比如Unsplash的图片或者用占位图服务。但要记住网络图片在小程序里可能不稳定尤其在国内建议下载到本地再用这也是我demo里全部用本地图片的原因。3.4 分包异步化与小程序体积控制关于“分包异步化”这个热搜老实说这个demo完全用不上因为主包体积很小。但不能不提因为一旦你开始往demo里堆图片和组件主包很快就会超过2MB这个时候就必须做分包。微信小程序的分包就是按照路由规则把页面拆分成主包和分包用户进入分包页面时再下载对应代码。如果你要让主包里的某个页面访问分包里的模块可以用“分包异步化”能力用require.async动态加载。不过对于新手建议先保持单包做好图片压缩减少不必要的库等包体真的超了再考虑分包。还有一个容易被忽略的体积问题是代码里不要console.log大对象虽然不影响上线体积但在调试时可能卡顿。做截图演示前记得把console面板清空不然截图里一片红色报错观感很差。3.5 购物车单选框与radio组件的坑购物车里用了“选中/不选中”的交互。我一开始直接用checkbox组件后来发现checkbox的默认样式很难看还不好对齐。如果你也在商城项目里需要类似功能建议用radio-group包裹或者更简单用一个透明的view盖住整个商品卡片绑定的点击事件去切换自定义的选中样式。比如这样view classcart-item bindtaptoggleSelect>.check-box { width: 40rpx; height: 40rpx; border: 2rpx solid #ccc; border-radius: 50%; margin-right: 16rpx; } .check-box.active { background-color: #ff4d4f; border-color: #ff4d4f; }这种“自定义单选框”的好处是样式完全可控也不会被原生组件默认padding干扰。注意点击事件绑定在父view上时如果子元素也有事件就容易冒泡。小程序里冒泡事件用bindtap会继续向外传递如果你需要阻止要用catchtap。比如数量加减按钮点击加号时不想触发整个卡片的选中/取消那么按钮上的事件就不能用bindtap而应该用catchtap。这个细节处理好了整个购物车交互会舒服很多。4. 源代码整理与截图演示技巧4.1 源代码怎么整理才不容易跑不起来写demo容易分享demo却是一个良心活。我见过很多项目源码下载下来跑不起来多半是缺少关键步骤说明或者代码路径依赖本机环境。所以我分享源码时会强制自己按照别人“零基础下载”的角度去整理。首先要贴一个README。README里写清楚项目介绍、运行环境要求、导入步骤、功能列表、目录结构。导入步骤要具体到“打开微信开发者工具-导入项目-选择miniprogram目录-填写AppID可以选测试号-点击确定”。不要觉得啰嗦很多新手卡在“找不到AppID”这一步。然后是目录。不要直接把整个开发工具生成的文件全部传上去至少要删掉node_modules如果有、.DS_Store等无关文件。为了保持代码最新建议把project.config.json和project.private.config.json也一并提交这样别人导入时可以保留你的编译设置。最后是版本管理。用Git从第一天开始一次提交一个功能点比如“feat: 完成首页商品列表”、“feat: 完成购物车缓存逻辑”。这样如果后面改坏了还能回滚也方便别人根据提交记录学习功能演变。4.2 截图怎么截才更有说服力源代码之外“截图”是这个项目标题里另外一个关键词。人们看开源项目第一眼永远是截图。如果你的截图是糊的、黑白灰的、没重点的用户根本不想继续看代码。我总结了几条截图经验选择微信开发者工具模拟器里的“iPhone 15 Pro”机型分辨率适中观感现代。固定一套主色调我用了红色作为按钮和价格的主色背景用浅灰整体干净。截取每个核心页面首页列表、商品详情、购物车数量调整、订单确认、订单列表。每个页面都要保证图片清晰、没有明显的布局错乱。真机截图也可以但要保证微信状态栏、底部tabBar没有遮挡主要内容。真机截图更真实但需要手动控制环境比较耗时。如果你要写博客或发布到社区建议把多张截图合成一张长图或者用“二维火”等拼图工具排列整齐。如果涉及用户数据记得打码。demo里没有真实用户信息所以还好。我还会在截图旁边加一两句文字说明不用长篇大论只写清楚“点击商品进入详情”“数量加减联动总价”这种关键交互阅读者一眼就知道这个demo做到了什么程度。4.3 从demo到真实商城还需要补什么这个demo是很好的起点但离可以上线的商城还有很长距离。我列一下真实项目里绕不开的几个点方便你后续扩展时心里有数。第一是登录。微信小程序推荐wx.login加code换openid然后把用户信息绑定到自己的服务端数据库。购物车、订单都要跟用户id关联。第二是支付。微信支付需要开通商户号并且小程序主体必须是企业或个体工商户个人主体无法开通支付。而且由于平台规则iOS端的微信小程序不能做虚拟商品的支付只能用于实物商品或服务。所以如果你做“知识付费”“会员充值”这类虚拟商品在设计demo时就要提前想好合规方案。第三是商品管理和订单管理。真实商城一定有后台商品上下架、库存管理、订单状态流转、物流信息都需要一套管理界面。刚开始可以用云开发够用后面数据复杂了再上独立后端。第四是搜索和分类。属于高频功能如果商品超过几十个没有搜索用户根本找不到东西。这些不是必须一次性做完但至少要有个意识demo是练手不是终点。4.4 常见问题速查表整理一份我平时处理小程序商城demo最常见的问题方便你排查问题原因解决方案真机预览白屏图片域名未配置、请求域名未配置、路径写错检查合法域名列表图片改用本地路径点击跳转没反应wx.navigateTo页面路径错误或未注册检查app.json的pages数组是否包含该路径购物车数量异常缓存数据格式不对清理缓存删除storage里cart字段后重进价格不更新没有调用getTotalPrice数量、选中、删除后都要触发总价计算tabBar不显示tabBar.list少于2项或者pagePath写错检查app.json配置tabBar页面必须是pages中已存在的路径复选框状态错乱使用index作为key且数组顺序变化用item.id作为wx:key不要用indexsetData报错更新了undefined或null属性处理接口数据异常情况先初始化data底部按钮遮挡内容未适配安全区使用env(safe-area-inset-bottom)遇到问题时先用“开发者工具-调试器-Console”看有没有红色报错然后用Source面板打断点定位是哪一行。大多数新手问题都出在路径和数据格式上把这两块理清楚能省下很多时间。5. 个人踩坑记录与demo扩展建议5.1 我写这个demo时踩过的三个坑这个demo看起来简单但我在写的时候也翻过车。第一个坑是首页与详情页的style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />