简介:微信小程序商店的完整实现代码,面向微信小程序开发者,尤其适合需要快速搭建应用商店或电商类小程序、研究商店整体架构的人群。压缩包共57个文件,体积仅131KB,代码轻量且结构完整,核心文件包括wxml页面结构、wxss样式、js业务逻辑、JSON配置与项目说明,另有png图标资源便于直接替换使用。已有4000余人浏览学习。开发者可通过页面布局与工具函数,学习小程序码解析、商品分类、搜索下载等典型功能的组织与调用方式,也可以抽取其中代码嵌入自己的项目中,减少从零开发的时间成本。整个项目目录划分清晰,按pages、utils、template等模块归类,方便对照阅读与二次开发。作为涵盖前端展示、后端服务与用户体验的完整参考,适合用于搭建自己的小程序商店或作为课程设计素材。
1. 微信小程序的商店代码:完整商城工程拿到手,先别急着改 UI
微信小程序商店完整代码这类资源网上一抓一大把,不少人拿到手先改 UI、换图片,改完才发现接口、数据模型、页面传值全是断的。这套代码的价值在于它把「首页-分类-商品详情-购物车-下单-支付-订单列表」这条链路完整串了起来,从全局配置、请求封装到支付回调都留出了可替换的口子。它适合三类人:刚接触小程序想抄一份完整作业的前端新人,给线下门店做商城的小团队,以及想跑通微信支付流程但不想从零搭骨架的二开党。先看骨架,再跑链路,最后改皮,是这套资源最不容易翻车的上手顺序。
2. 拆开骨架看结构:目录分层、数据模型与请求封装的三个关键点
新手打开完整代码工程,常见动作是一页一页翻 pages 目录,翻完感觉“挺全的”,但真到要改需求时不知道从哪下手。我建议大家先拆三层:全局层(app.js、app.json、app.wxss)、工具层(utils 下的 request、auth、format 等公共模块)、页面层(pages 下每个页面各自的 js、json、wxml、wxss)。把这三层边界划清楚,后面改接口、改样式、加页面才不会互相踩。这套代码里最应该先读的三个文件,就是 app.json、utils/request.js 和 app.js,它们决定了整个工程的路由、网络和数据底座。
2.1 页面注册与 TabBar 配置:app.json 决定的启动路由
微信小程序编译时第一个找的就是根目录的 app.json,它不只是样式配置,还承担了页面注册和底部导航的定义。很多人以为某个页面文件名带 index 就会被当成默认首页,实际上 pages 数组里排第一的才是冷启动的落地页,顺序错了,打开小程序直接进到购物车页或者订单页,体验会很奇怪。
{ "pages": [ "pages/index/index", "pages/category/category", "pages/cart/cart", "pages/user/user", "pages/goods_detail/goods_detail", "pages/order_confirm/order_confirm", "pages/order_list/order_list", "pages/pay_result/pay_result" ], "window": { "navigationBarBackgroundColor": "#ffffff", "navigationBarTitleText": "小店", "navigationBarTextStyle": "black", "backgroundColor": "#f6f6f6" }, "tabBar": { "color": "#9b9b9b", "selectedColor": "#e64340", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/category/category", "text": "分类" }, { "pagePath": "pages/cart/cart", "text": "购物车" }, { "pagePath": "pages/user/user", "text": "我的" } ] } }逻辑说明:pages 数组是这个工程的完整路由表,所有能访问的页面都必须在这里注册。tabBar 里挂的页面必须同时出现在 pages 数组里,而且 tabBar 页面不能用 wx.navigateTo 去跳,只能通过 switchTab,这是个高频报错点。navigationBarTextStyle 只支持 black 和 white 两个枚举值,写成其他样式会被真机忽略。
参数说明:navigationBarBackgroundColor 只收十六进制色值,不支持 rgb() 写法。很多人只改了 wxss 里的页面背景色,忘了改 window.backgroundColor,真机上拉页面露出底部的白边,多半就是这里没对齐。tabBar 里的 pagePath 要和 pages 数组的路径完全一致,少一个点、多个斜杠都会直接导致编译报错。
2.2 globalData 与本地缓存的边界:哪些数据该放哪里
这套代码里数据存放有三种位置:globalData 存启动时需要全局共享的信息,wx.setStorageSync 存需要跨页面持久化的业务数据,页面 data 存当前页面的渲染状态。新手经常把购物车塞进 globalData,这是隐患:小程序进程可能随时被系统回收,globalData 不持久,下一次冷启动数据就没了。买过的东西全清空,用户会直接弃用。
// app.js App({ globalData: { shopInfo: null, token: '', baseURL: 'https://api.example-store.com' }, onLaunch() { // 启动时先恢复登录态和购物车 const token = wx.getStorageSync('token') const cart = wx.getStorageSync('cart') if (token) { this.globalData.token = token } if (!cart) { // 首次打开给购物车一个空数组,避免后续页面直接 map 报错 wx.setStorageSync('cart', []) } // 拉取店铺信息,用于全局展示店铺名称和联系方式 this.loadShopInfo() } })逻辑说明:onLaunch 阶段同步读取本地缓存,避免后续页面 onLoad 时拿不到数据。购物车初始化为空数组这一步很关键,很多“白屏 + xxx is not a function”的报错,就是前面某次写入把购物车格式写坏,后面页面当数组遍历时报出来的。token 是登录后的身份凭证,后端接口对它做校验,失效时请求层应该统一拦截,这个链路稍后在请求封装里展开。
参数说明:wx.getStorageSync 是同步 API,适合启动阶段的小体量读取;数据量大了以后要考虑异步版 wx.getStorage 或者直接走服务端接口。baseURL 放在 globalData 里不是长久之计,更干净的做法是单独建一个 config.js,把域名、版本号、开关集中管理,后面换环境只改一个文件。
2.3 核心数据模型设计:商品、购物车项、订单的结构约定
这套商店代码里,数据模型直接决定页面遍历的写法。购物车里每一项应该是一个扁平对象:goodsId、skuId、name、price、image、count、stock。后端返回的商品对象可能嵌套很深,进入购物车之前一定要先做一次扁平化映射,否则页面取价格还要一层一层点属性,写起来痛苦,改起来更是灾难。
| 数据对象 | 关键字段 | 用途说明 |
|---|---|---|
| 商品 | id, title, cover, price, stock, skuList | 页面卡片展示与详情页规格选择 |
| 购物车项 | goodsId, skuId, name, price, image, count, stock | 购物车列表渲染和结算计算 |
| 订单 | orderId, status, totalAmount, goodsList, addressId | 订单列表展示和支付回调 |
购物车项的 count 表示商品数量,stock 是下单瞬间的库存快照,结算前要用它做一次前端拦截。price 建议用整数分存储,展示时再除以 100 并保留两位小数,避免浮点运算出现 0.1 + 0.2 之类的精度玄学问题。如果后端返回的是元单位浮点数,那就在请求层统一做一次转换,不要在页面里到处乘除。
逻辑说明:这张表不是随便列的,每个字段都能在 wxml 里的数据绑定中找到对应位置。比如订单列表页只需要 orderId、status、totalAmount 三个字段,用户中心页依赖 status 渲染待付款、待发货、待收货的角标。字段少了页面缺数据,字段多了接口传输有冗余,这套代码里的字段约定基本是按常见商城 H5 那套模型迁移过来的,二开时最好保持结构不变。
2.4 请求层封装:把 wx.request 包装成可拦载、可替换的 Promise 模块
整套代码里最值得复用的就是 utils/request.js。它做的事情不算复杂,但把 wx.request 包成 Promise 绕开了回调地狱,同时统一处理登录态携带、业务码判断、401 跳转和错误提示。拿到这套代码后,第一件要改的就是这个文件的 baseURL,以及确认后端接口的返回结构是否是约定的 code + data + message。
// utils/request.js const config = require('../config') function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: config.BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', // token 同步带上,后端从这里解析用户身份 Authorization: wx.getStorageSync('token') || '' }, success(res) { // 约定:statusCode 200 且 code=0 表示业务成功 if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data) } else if (res.statusCode === 401) { // 登录态失效:清缓存、跳登录页 wx.removeStorageSync('token') wx.navigateTo({ url: '/pages/login/login' }) reject(new Error('login expired')) } else { wx.showToast({ title: res.data.message || '请求失败', icon: 'none' }) reject(new Error(res.data.message)) } }, fail(err) { // 网络层失败:断网、域名未配置、证书问题等 wx.showToast({ title: '网络异常,请稍后重试', icon: 'none' }) reject(err) } }) }) } module.exports = { request }逻辑说明:把 wx.request 包成 Promise 是为了配合 async/await 串起复杂流程,典型场景是先获取收货地址,再计算运费,最后提交订单。Authorization 字段从本地缓存里取,不用每次调接口临时传 token。fail 回调里最常见的报错是 request:fail url not in domain list,说明请求域名没在小程序后台配置,或者开发工具里开了域名校验。
参数说明:method 默认值写在函数签名里,页面里不传就自动用 GET。code=0 是这套代码约定的业务成功标记,如果你们后端用的是别的字段名,比如 success=true,只需要改这一处判断即可。401 统一做跳转,避免每个页面自己写一遍 token 过期逻辑。如果项目里还有上传文件的需求,可以在同目录加一个 upload.js,复用同样的 header 和错误提示逻辑。
3. 把核心链路跑通:商品列表、购物车联动与下单支付的页面代码
看完全局再进页面,这套代码里信息量最大的是三个页面:首页/分类页的商品列表、购物车页的数量与金额联动、下单确认页的地址与支付串联。把这三段代码吃透,基本上整个商店的主流程你就拿下了。改起来最顺手的顺序是:先跑通列表展示,再调购物车逻辑,最后连支付。
3.1 首页与商品列表分页加载:onReachBottom 触底续载的写法
首页看起来只是淘几个商品卡片,实际上隐藏了分页加载、下拉刷新、加载状态保护三套逻辑。很多二开版本这里最容易出现的问题是一口气拉全量商品,或者触底之后重复请求,所以看代码时重点盯着 page、pageSize、loading、finished 这四个标记位。
// pages/index/index.js const { request } = require('../../utils/request') Page({ data: { banners: [], goodsList: [], page: 1, pageSize: 8, loading: false, finished: false }, onLoad() { this.loadBanners() this.loadGoods(true) }, async loadGoods(reset = false) { // reset 为 true 表示下拉刷新,从头开始拉 if (this.data.loading || this.data.finished) return this.setData({ loading: true }) const page = reset ? 1 : this.data.page try { const res = await request(`/goods/list?page=${page}&pageSize=${this.data.pageSize}`) const list = res.list || [] this.setData({ goodsList: reset ? list : this.data.goodsList.concat(list), page: page + 1, loading: false, finished: list.length < this.data.pageSize }) } catch (e) { this.setData({ loading: false }) } }, onReachBottom() { // 触底时追加下一页 this.loadGoods() }, onPullDownRefresh() { this.loadGoods(true).then(() => wx.stopPullDownRefresh()) } })逻辑说明:loadGoods 接收一个 reset 参数,下拉刷新时重置页码从 1 开始,触底加载时用当前页码继续往后取。loading 和 finished 是两道闸,前者防止快速滚动时多次触发请求,后者判断列表拉完就停止后续请求。concat 拼接数组而不是整体覆盖,这样触底加载不会丢掉前面已有的数据。
参数说明:pageSize 设置成 8 只是示例,具体要根据后端接口的分页上限来调,设太大会导致首屏渲染变慢。finished 的判断依据是 list 长度小于 pageSize,意味着最后一页没拉满,反推已经没有更多数据。如果后端的分页字段是 pageNum 而不是 page,记得在请求层做参数名转换,页面代码保持不变。
3.2 购物车数量变更:setData 按路径更新与金额重算
购物车页面看着简单,实际是整套代码里最容易出错的地方。数量加减如果整页 setData,在商品多的时候会明显卡顿;金额计算如果依赖页面里某个过期的 data 字段,就会出现数量改了金额不动、库存超了还在加的黑匣子问题。这套代码的处理方式是“只改数组里的那一项,再单独重算金额”。
// pages/cart/cart.js Page({ data: { items: [], selectedIds: {}, totalPrice: '0.00', selectedCount: 0 }, onShow() { // 每次从缓存里读取,保证购物车角标和列表同步 this.setData({ items: wx.getStorageSync('cart') || [] }) this.calcTotal() }, changeCount(e) { const { id, delta } = e.currentTarget.dataset const items = this.data.items const idx = items.findIndex(item => item.id === id) if (idx === -1) return const nextCount = items[idx].count + delta const stock = items[idx].stock || 99 if (nextCount < 1) return if (nextCount > stock) { wx.showToast({ title: '库存不够啦', icon: 'none' }) return } // 只更新数组里这一项的 count,避免整页重绘 this.setData({ [`items[${idx}].count`]: nextCount }) wx.setStorageSync('cart', this.data.items) this.calcTotal() }, calcTotal() { const items = this.data.items let total = 0 let count = 0 items.forEach(item => { if (this.data.selectedIds[item.id]) { total += item.price * item.count count += item.count } }) this.setData({ totalPrice: total.toFixed(2), selectedCount: count }) } })逻辑说明:changeCount 通过 dataset 拿到商品 id 和增量 delta,先检查数量和库存边界,再通过计算属性路径items[${idx}].count精准更新目标项。setData 支持数组下标的字符串路径,这是官方文档里明确写过的性能优化点。wx.setStorageSync 紧跟其后把最新数组写回缓存,这样即使页面被系统回收,数据也不会丢。
参数说明:delta 在 wxml 里通过>// pages/order_confirm/order_confirm.js const { request } = require('../../utils/request') Page({ data: { address: null, goodsList: [], totalAmount: 0, postage: 0, submitting: false }, async submitOrder() { if (this.data.submitting) return if (!this.data.address) { wx.showToast({ title: '请先选择收货地址', icon: 'none' }) return } const goodsListSnapshot = this.data.goodsList.map(g => ({ goodsId: g.goodsId, skuId: g.skuId, count: g.count })) this.setData({ submitting: true }) try { const res = await request('/order/create', 'POST', { addressId: this.data.address.id, goodsList: goodsListSnapshot, remark: this.data.remark || '' }) // 后端返回支付参数,直接透传给 wx.requestPayment const pay = res.payParams wx.requestPayment({ timeStamp: pay.timeStamp, nonceStr: pay.nonceStr, package: pay.package, signType: 'MD5', paySign: pay.paySign, success: () => { // 支付成功后清空购物车,跳转支付结果页 wx.removeStorageSync('cart') wx.redirectTo({ url: `/pages/pay_result/pay_result?orderId=${res.orderId}` }) }, fail: (err) => { // 用户取消或签名错误都会进这里,具体看 err.errMsg console.error('pay fail', err) wx.showToast({ title: '支付未完成', icon: 'none' }) this.setData({ submitting: false }) } }) } catch (e) { this.setData({ submitting: false }) } } })
逻辑说明:submitting 这个布尔量是防止重复下单的开关,网络慢的时候用户连续点按钮,没有这层拦截会生成多个重复订单。goodsListSnapshot 在提交那一刻固化商品快照,后续即使购物车被清空或商品下架,订单里的数据依然完整。payParams 是后端根据 openid、商品金额、订单号生成的签名参数,小程序端只是透传,不要在本地组装这些字段。
参数说明:wx.requestPayment 的 package 字段必须先经过 encodeURIComponent 编码,很多后端 SDK 已经处理过这一步,但如果你们后端是 Java 或 PHP 原生拼的,前端拿到后要检查是否多转码了一次,否则真机上会报 package 参数错误。fail 回调里如果 errMsg 包含 cancel,说明用户主动取消支付,此时不应该提示网络错误。signType 通常和商户平台配置一致,不一定是 MD5,也有 HMAC-SHA256,必须跟后端对齐。
4. 避坑与排查:五个高频问题从现象到根因到解决
代码能跑是一回事,真机跑得稳是另一回事。这套商店代码我在模拟项目 X 里完整跑过一轮,也帮 A 同学排查过不少二开引入的问题。下面这五个坑,几乎把所有拿到源码后半夜不睡觉的开发者都捞过一遍,每一条都按现象、原因、解决来给。
4.1 商品图片裂了:后台返回的图片 URL 没做协议与域名处理
现象:开发工具里商品图显示正常,真机预览时一部分图片裂开,控制台报 downloadFile:fail url not in domain list,还有一部分是 http 协议的图,点开直接是空白占位。
原因:图片 URL 有两种问题。后端返回的图片地址可能是 //img.example-store.com/a.jpg 这种协议相对地址,真机渲染时拿不到合法的 https 协议头;更常见的是图片域名没加到小程序的 downloadFile 合法域名列表里。开发工具默认可以关闭域名校验,真机是强制校验的,两者表现不一致,这也是“开发工具好好的,真机全裂了”的经典翻车现场。
解决:在请求层或者数据映射层保留一个统一的图片地址处理函数,给相对路径自动补 https 协议;同时把图片域名在小程序后台的「开发管理-服务器域名-downloadFile 合法域名」里配好。这一条最容易被忽略,大部分二开者都只配了 request 合法域名,忘了图片和视频走的是 downloadFile。
4.2 购物车数量改了金额不刷新:setData 路径和数据缓存不同步
现象:购物车里点加减号,数量数字变了,总价纹丝不动;退出页面重新进来,金额突然又对了。
原因:这是两个问题叠在一起。数量字段通过计算属性路径更新了,但 calcTotal 读取的可能是 this.data 里的旧缓存数组,两者不同步;另一处是 onShow 里先 setData 了新数据,紧接着 calcTotal 计算完又整体 setData 了一次,结果顺序错乱,后一次把前面的计算结果覆盖了。
解决:把 calcTotal 里的读取源统一改为 this.data.items 的同一份引用,数量变更后立刻执行重算并从 storage 里重新读出最新值来兜底。要点是“改一条路径就重算一次”,不要等到某个生命周期回调里再统一算。我后来写的购物车组件,calcTotal 永远只依赖一个统一的 state 对象,不存在两份数据打架的问题。
4.3 真机支付没反应:支付参数签名或异步通知地址不对
现象:开发工具里点支付弹窗正常拉起,真机上一遍又一遍点了没反应,console 里出现 errMsg: requestPayment:fail invalid sign。
原因:微信支付签名细节非常多,常见是后端生成 paySign 时参与签名字段顺序不对,或者 timeStamp 类型传成了字符串,导致真机验签不通过。开发工具对部分参数容忍度较高,真机签不过就静默失败。另一个容易被忽略的是支付结果异步通知地址,后端没配或没公网可达,支付成功但订单状态一直不改。
解决:先抓一份正常支付请求的参数做前后端字段逐项比对,重点看 timeStamp 的值类型、nonceStr 是否同一个字符串、package 是否编码。真机调试时打开 vConsole,把 fail 回调里的 errMsg 完整打出来,不要只看 toast。后端异步通知地址必须填公网 HTTPS 接口,本地联调只能依赖 devtools 里的模拟支付功能。
4.4 分享卡片打开是空白页:onLoad 的 options 参数丢失或格式错误
现象:从会话里点别人转发的卡片,小程序打开后停留在启动页,或者进入商品详情页但数据是空的,一片白。
原因:分享卡片路径是 pages/goods_detail/goods_detail?goodsId=123 这种带 query 的地址。问题要么是分享时路径拼错,要么是把参数写进了 onShareAppMessage 的 path 里但没编码,蹭到中文字符或空格之后真机直接解析失败。页面端如果只在 onLoad 里读了一次 options,冷启动时参数延迟也会造成空数据。
解决:分享的 path 参数统一用 encodeURIComponent 包装后再拼接,页面 onLoad 里对 options 里的字段做一次 decodeURIComponent。如果是分享带商品分类或活动来源的,建议在 onLoad 和 onShow 里做一次参数合并,防止冷启动和热启动场景表现不一致。我自己写分享逻辑时,还会在页面里加一个参数为空时的兜底跳转,避免白屏没有任何反馈。
4.5 安卓端样式错位:rpx 与 px 混用导致布局崩掉
现象:同一套代码 iOS 手机上显示完美,安卓真机上商品卡片参差不齐,有的边框粗一圈,有的文字被截断。
原因:rpx 是微信小程序的动态单位,按屏幕宽度自适应;px 是固定像素,在安卓设备上物理分辨率差异大,混用时会导致盒子模型撑破。更多情况是模板里写了 inline style 用了 px,或者 wxss 里写的 border 宽度、字体大小没有统一走 rpx。iOS 和安卓对小数像素的处理机制不同,rpx 换算后出现 0.5 像素时,安卓会四舍五入,iOS 会保留,最终看起来就是各种对不齐。
解决:全项目搜索 wxss 里单独的 px 单位,除了 1px 的 border 和某些需要固定物理像素的场景,其余一律改成 rpx。注意 iPhone X 底部安全区这类特殊情况,要用 env(safe-area-inset-bottom) 配合 constant 兼容,而不是靠固定的 px 回避。改完样式后在安卓低端机和 iOS 各过一遍核心页面,别只在 iPhone 上调完就提交代码。
5. 上线前最后一个技巧:用 Mock 数据完整跑通链路,再切真实接口
拿到代码后最稳的开发顺序,是先用这套代码自带的 Mock 数据把页面链路全部跑一遍,然后再切真实接口。切接口前,我会在 config 里加一个 USE_MOCK 开关,让它决定 request 层走本地模拟还是真实网络。这一步的价值在于:不用等后端出接口,前端先把页面交互、支付弹窗、订单回跳全部验证一遍,把问题暴露在前端自己身上。
// config.js module.exports = { BASE_URL: 'https://api.example-store.com', USE_MOCK: true }// utils/request.js 开头增加一段拦截 const config = require('../config') const mock = require('./mock') function request(path, method, data) { if (config.USE_MOCK && mock[path]) { // Mock 模式下不发真实网络请求,直接返回本地数据 return Promise.resolve(mock[path](method, data)) } return realRequest(path, method, data) }Mock 数据文件里按接口路径组织,比如 /goods/list 返回写死的商品数组,/order/create 返回一个假的 payParams。订单创建这种接口,Mock 模式不需要真的调起支付,只要保证 payParams 里每个字段都有值,页面渲染不报错即可。开发工具里把「模拟支付」开关打开,真机验证支付时再关闭 Mock 切回真实签名。
验证清单我一般固定四步:第一步 Mock 模式跑通商品列表、购物车、下单、支付结果页全链路;第二步切真实接口、关 Mock,验证商品图片加载和分页;第三步真机调试支付回调;第四步清掉所有缓存从分享卡片冷启动一次。从那以后我每接一个商城小程序,都会先把这套 Mock 开关做好再开始改业务。支付和分享这两个环节,等后端接口齐了再联调,时间成本完全不一样。希望帮到你。
本文还有配套的精品资源,点击获取