
简介微信小程序商城项目实战是一份完整的电商类小程序源码学习包面向正在学习微信小程序开发或准备独立完成商城项目的开发者。压缩包内共90个文件涵盖22个js脚本、22个json配置、20个wxss样式、18个wxml页面结构及8个png图标资源整体约109KB从全局配置、页面路由、自定义组件到工具模块一应俱全。项目覆盖首页、商品列表、商品详情、购物车、订单、支付、用户中心、地址管理、搜索、收藏等典型电商模块并包含wx.request网络请求、本地缓存、用户授权登录、性能优化等关键知识点的实际落地写法。已有2374人学习下载适合用作毕业设计、课程项目或小程序商城开发的参考蓝本可直接对照运行、二次开发快速理解小程序电商项目的完整架构与业务实现。1. 微信小程序商城项目的构成与拆解思路拿到这份微信小程序商城源码时第一感觉是页面划分相当完整从首页、分类、商品列表、详情、购物车、地址、订单、支付到登录和反馈电商主链路几乎全部覆盖。压缩包里的pages目录按业务拆分页面components下抽出了Tabs和SearchInput两个自定义组件utils里放了request.js、mock.js和util.js说明作者是认真考虑过复用和模拟数据问题的。对于想快速搭一个商城小程序、或者正在做课程设计但不知道怎么组织页面结构的人来说这份源码比散落的 Demo 更有参考价值。本文会从全局配置、页面路由、组件通信、商品链路和数据请求、交易链路这个顺序逐层拆解最后把登录、收藏、反馈这些收尾逻辑一并讲清楚中间所有代码都能直接对着源码目录找到对应文件。2. 小程序商城的全局骨架app.json、tabBar 与页面路由2.1 从 app.json 看页面注册顺序与启动页微信小程序里所有页面必须在app.json的pages数组中登记数组第一项就是启动页面。这份源码的app.json把首页pages/index/index排在最前意味着小程序冷启动后直接进入商城首页这是电商项目的标准做法毕竟用户进来第一眼看到的是商品而不是登录页。{ pages: [ pages/index/index, pages/auth/auth, pages/search/search, pages/feedback/feedback, pages/cart/cart, pages/logs/logs, pages/address/address, pages/category/category, pages/user/user, pages/goods_list/goods_list, pages/addressList/addressList, pages/goods_detail/goods_detail, pages/collect/collect, pages/pay/pay, pages/order/order, pages/login/login ], window: { backgroundTextStyle: light, navigationBarBackgroundColor: #ff2d4a, navigationBarTitleText: 优购商城, navigationBarTextStyle: white }, tabBar: { color: #999, selectedColor: #ff2d4a, list: [ { pagePath: pages/index/index, text: 首页, iconPath: icons/home-o.png, selectedIconPath: icons/home.png }, { pagePath: pages/category/category, text: 分类, iconPath: icons/category-o.png, selectedIconPath: icons/category.png }, { pagePath: pages/cart/cart, text: 购物车, iconPath: icons/cart-o.png, selectedIconPath: icons/cart.png }, { pagePath: pages/user/user, text: 我的, iconPath: icons/my-o.png, selectedIconPath: icons/my.png } ] } }这段配置把商城最核心的四个入口固定在了底部 tabBar首页、分类、购物车、我的。iconPath指向icons目录下的灰色图标selectedIconPath指向选中态的红色图标图标文件用-o.png后缀区分未选中和选中状态这是一种命名约定后面自己加 tab 的时候照着这个规则放图就不会乱。2.2 tabBar 页面与普通页面的路由差异tabBar 支持四个页面这个数量是微信的硬性限制不能多也不能少。源码里search、goods_list、goods_detail、pay、order这些页面没有出现在 tabBar 中它们属于业务子页面需要通过wx.navigateTo跳转进入。navigateTo 跳转的页面会压入页面栈页面栈最多十层超过之后wx.navigateTo会静默失败这是商城项目里最常见的路由坑之一比如用户从首页进分类、分类进列表、列表进详情、详情再进支付连续跳转超过十次就会出现跳转无效需要在中途用wx.redirectTo替换当前页面。在pages/user/user.js这类页面中比较稳妥的做法是先用wx.navigateTo去登录页拿到登录态之后用wx.navigateBack返回这样页面栈深度始终可控。// pages/user/user.js 中的跳转逻辑 goLogin: function () { wx.navigateTo({ url: /pages/login/login }); }url必须以/开头写绝对路径这是 wx.navigateTo 的硬性要求写相对路径在部分基础库版本会出现跳转白屏。2.3 sitemap.json 与 project.config.json 的配置含义project.config.json是开发者工具读取的项目配置里面包含appid、projectname、libVersion等字段。拿到源码后第一件事就是把appid替换成自己的测试号或企业号否则工具会报“appid 不存在”的错误。project.private.config.json是个人私有配置通常包含个人的本地设置提交代码时一般会通过.gitignore排除但如果直接解压源码运行这个文件有时候会覆盖公共配置导致编译异常遇到诡异问题可以先把私有配置删掉再试。sitemap.json配置小程序的页面索引规则默认是{rules: [{action: allow, page: *}]}表示所有页面允许被微信索引。商城项目里支付页、登录页这类敏感页面更合理的做法是单独设置为disallow但要注意 sitemap 只影响微信搜索的页面收录不影响页面正常访问。3. 商城自定义组件Tabs 与 SearchInput 的复用逻辑3.1 为什么要抽 Tabs 组件商品列表页goods_list里通常有“综合”“销量”“价格”三个排序 Tab分类页category左侧有分类导航这些场景的 UI 都是顶部一排可点击的选项选中态和未选中态需要同步切换。如果在每个页面都复制一份同样的视图和交互代码后续想改选中颜色要同时改好几个文件。源码里把这块逻辑抽成了components/Tabs/Tabs组件页面只需要传入标题数组然后监听点击事件。// components/Tabs/Tabs.js Component({ properties: { tabs: { type: Array, value: [] } }, data: { currentIndex: 0 }, methods: { handleTap: function (e) { const index e.currentTarget.dataset.index; this.setData({ currentIndex: index }); this.triggerEvent(itemChange, { index }); } } });组件通过properties声明外部传入的tabs数组这里的type: Array是类型校验如果外部传了字符串控制台会告警。点击时先更新组件内部的高亮索引然后通过triggerEvent把index抛给父页面。这样组件内部只管自己长什么样数据变化之后父页面要请求哪个排序的商品列表完全由父页面自己的逻辑决定。Tabs 组件的 WXML 结构里wx:for循环渲染标题>!-- components/SearchInput/SearchInput.wxml -- input classsearch-input placeholder{{placeholder}} bindinputhandleInput bindconfirmhandleConfirm /组件里把placeholder也暴露成了属性这样首页可以做“搜索优购商品”分类页可以做“搜索分类商品”复用性更强。输入事件和确认事件通过triggerEvent抛给父页面父页面拿到关键词之后跳转到搜索页或者直接请求搜索接口。组件化有一个容易被忽略的点input 组件在自定义组件里使用时如果组件没有监听bindinput用户输入的内容不会反馈到父页面因为 input 的值本身是组件内部状态。所以像购物车数量这种需要持久化的数据不能把 input 放在组件里就完事必须通过事件冒泡把值传出来。3.3 组件通信的三种方式对比商城项目里组件通信除了properties和triggerEvent还有两种手段父页面通过selectComponent直接调用组件实例的方法以及通过全局数据总线。在goods_list页面里如果搜索框组件需要被外部重置可以在父页面用this.selectComponent(#search)拿到组件实例直接调用setData修改组件数据。但这种方式破坏了单向数据流调试的时候数据流向不清晰我一般建议能用properties 事件解决的场景不要轻易用selectComponent。通信方式适用场景数据流向缺点properties triggerEvent父子组件之间常规交互单向清晰复杂双向绑定代码量大selectComponent获取组件实例调用内部方法反向不直观组件间耦合变高全局变量或 storage跨页面共享状态如登录态任意方向数据变更不响应需要手动同步购物车这种多页面共享的数据比较特殊源码里的做法是通过wx.setStorageSync和wx.getStorageSync做本地缓存页面onShow时重新读取相当于用缓存做了一个简单的全局状态管理。这种方法简单可靠但需要自己控制缓存更新的时机否则会出现购物车角标数字和实际数据不一致的问题。4. 商品链路实战mock 数据、request 封装与列表详情联动4.1 mock.js 构造商品数据的思路商城项目如果后端接口还没就绪前端开发会被阻塞。源码里的utils/mock.js就是为了解决这个问题它模拟了一批商品数据包括商品 id、名称、图片、价格、库存和销量字段页面直接引用这份数据渲染列表等后端接口好了再把数据源切换成网络请求。// utils/mock.js const goods [ { id: 1, name: 纯棉短袖T恤, price: 59.9, image: /icons/home.png, stock: 200, sales: 1200 }, { id: 2, name: 运动休闲鞋, price: 299, image: /icons/category.png, stock: 80, sales: 566 }, { id: 3, name: 双肩电脑背包, price: 159, image: /icons/cart.png, stock: 150, sales: 899 } ]; function getGoodsList(filter) { let list [...goods]; if (filter filter.sort) { if (filter.sort price) { list.sort((a, b) a.price - b.price); } if (filter.sort sales) { list.sort((a, b) b.sales - a.sales); } } return list; } module.exports { getGoodsList };...goods是 ES6 展开语法目的是复制一份新数组而不是直接修改原数组避免了排序时污染原始数据。getGoodsList函数接收一个filter对象目前支持price和sales两种排序商品列表页点 Tab 切换时调用的就是这个函数。这种 mock 方案的优点是页面代码写法和真实请求几乎完全一致后面换成wx.request时只需要改request.js内部实现页面不用大改。4.1.1 为什么 filter 参数单独设计成对象排序条件未来可能扩展成价格区间、品牌筛选、关键词搜素等如果函数签名写成getGoodsList(sort)增加参数时所有调用方都要改。用对象传参可以做到向后兼容新增字段不影响已有调用。这种设计是接口设计里的通用做法不只是 mock 函数后面封装request.js时也建议沿用这种对象传参风格。4.2 request.js 封装 wx.request 的统一入口真实商城的商品数据不会写死在本地utils/request.js在这里承担了请求统一封装的作用。源码里的 request.js 把wx.request包了一层统一处理 baseURL、请求头、超时时间和错误提示。// utils/request.js const BASE_URL https://api.example.com; function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, timeout: 10000, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.statusCode 401) { wx.navigateTo({ url: /pages/login/login }); reject(res); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res); } }, fail(err) { wx.showToast({ title: 网络请求失败, icon: none }); reject(err); } }); }); } module.exports { request };这里用 Promise 包装wx.request调用方可以用async/await写出同步风格的代码可读性比回调嵌套好很多。默认Content-Type设置为application/json如果后端接口需要表单提交必须在调用时显式覆盖header。Authorization直接从本地缓存读 token实现登录态自动携带这是商城接口鉴权的常用套路。401 状态码表示登录过期统一跳转到登录页。4.2.1 Promise 封装里的一个边界问题wx.request的success回调只在网络层收到响应时触发但 HTTP 200 不代表业务成功。这里的判断逻辑是res.data.code 0才 resolve意味着后端必须在返回体里有一个业务状态码字段。如果你们后端约定的是status而非code拿到这份源码后要同步改掉这里的字段名否则每个接口都会走进“请求失败”分支。4.3 商品列表页跳转详情的参数传递goods_list页面点击任一商品卡片会跳转到goods_detail页面电商场景下这里必须把商品 id 传过去详情页拿这个 id 去请求对应的商品数据。// pages/goods_list/goods_list.js goDetail: function (e) { const id e.currentTarget.dataset.id; wx.navigateTo({ url: /pages/goods_detail/goods_detail?id id }); }// pages/goods_detail/goods_detail.js onLoad: function (options) { this.setData({ goodsId: options.id }); this.getGoodsDetail(options.id); }wxml里绑定数据时要用>// pages/cart/cart.js 中更新购物车数量的核心逻辑 updateCount: function (e) { const index e.currentTarget.dataset.index; const type e.currentTarget.dataset.type; const cart this.data.cart; const item cart[index]; if (type plus) { if (item.count item.stock) { wx.showToast({ title: 库存不足, icon: none }); return; } item.count; } else if (type minus) { if (item.count 1) { wx.showToast({ title: 至少购买一件, icon: none }); return; } item.count--; } this.setData({ cart }); this.calcTotal(); }这里直接在原数组上修改了item.count然后setData整个cart数组。在小程序里直接修改对象属性再赋值给setData是可以的因为this.data.cart本身就是数组引用修改内部对象再传回去能触发渲染。但要注意setData传整个数组在数据量大时会有性能问题更精细的做法是用setData的 key path 语法比如this.setData({ [cart[ index ].count]: item.count })只更新变更的那一项。stock库存限制和最小值 1 的限制是电商购物车的基础校验这里用return提前退出避免后续无效计算。每次数量变化后重新计算总价计算逻辑放在calcTotal方法里遍历购物车数组将勾选商品的单价乘数量累加。5.2 本地缓存与购物车数据持久化小程序应用被杀掉之后内存里的购物车数据会丢失所以每次变更都要同步到本地缓存。源码里在购物车的onLoad和onShow中读取缓存每次增删改后写缓存。缓存 key 的定义看起来很简单但这是整套数据持久化方案的关键。缓存 key存储内容写入时机读取时机cart购物车商品数组增删改、勾选状态变化购物车页 onShowtoken登录凭证登录成功request 请求头userInfo用户信息对象登录授权成功用户中心展示addressList收货地址数组新增、编辑、删除地址地址列表 onShow购物车在onShow里重新读取缓存而不是在onLoad里读原因是onLoad只在页面首次创建时执行一次而从商品详情页加入购物车后返回购物车页只是从后台恢复到前台不会重新触发onLoad只有onShow每次从后台切回前台都会执行。这个差异是购物车数据不刷新问题的根源也是 tabBar 页面和普通页面的生命周期差异在实际项目中最典型的反映。// pages/cart/cart.js onShow 中读取缓存 onShow: function () { const cart wx.getStorageSync(cart) || []; this.setData({ cart }); this.calcTotal(); }读缓存后用|| []兜底避免第一次使用小程序时缓存不存在导致setData收到 undefined。5.3 订单与支付流程的状态处理购物车页面点“去结算”会跳转到pages/pay/pay这个页面负责展示订单商品清单、计算应付总额并把订单信息落地。源码里pay.js的核心工作是把购物车勾选的商品组装成订单数据结构写入订单列表缓存然后清空购物车里对应的商品。// pages/pay/pay.js 中的结算逻辑 submitOrder: function () { const selectedGoods this.data.cart.filter(item item.selected); if (selectedGoods.length 0) { wx.showToast({ title: 请选择商品, icon: none }); return; } const order { id: Date.now(), goods: selectedGoods, totalAmount: this.calcTotal(), status: pending, createTime: new Date().toLocaleString() }; const orders wx.getStorageSync(orders) || []; orders.unshift(order); wx.setStorageSync(orders, orders); const remainingCart this.data.cart.filter(item !item.selected); wx.setStorageSync(cart, remainingCart); }filter把勾选商品和非勾选商品拆成两批勾选的进订单没勾选的留在购物车。Date.now()生成订单 id精度到毫秒并发情况下可能导致 id 重复正式的商城系统会由后端生成唯一订单号。orders缓存里unshift插入新订单这样订单列表页展示时最新的在前面不需要再单独排序。订单状态这里只有pending一种源码没有实现完整的支付回调链路。真实场景下wx.requestPayment需要后端生成支付参数包括timeStamp、nonceStr、package、signType、paySign小程序端拿到这些参数调用支付然后通过success回调判断支付结果支付完成后订单状态从pending变成paid。这份源码把订单状态流转简化了但数据结构上预留了status字段自己接真实支付时在这个字段上做状态机扩展即可。order页面读取订单缓存时注意不要直接修改缓存里的对象再写回去正确做法是先JSON.parse(JSON.stringify())深拷贝一份再操作否则会污染缓存数据导致下次读取时数据格式异常。5.4 地址管理address 与 addressList 的职责划分源码里有两个地址相关页面pages/address/address和pages/addressList/addressList。前者是新增/编辑地址的表单页后者是地址列表展示和选择页。地址列表页从缓存读取addressList数组渲染每条地址记录包含收货人name、电话phone、详细地址detail、是否默认isDefault字段。新增地址表单校验时要重点处理手机号格式正规做法是用正则/^1[3-9]\d{9}$/验证源码的 address.js 里应该有类似的校验逻辑。默认地址的处理逻辑比较常见新设置的默认地址要先清除其他地址的isDefault字段否则会出现多个默认地址并存。这个逻辑写在addressList.js中遍历数组重置标记后再给当前项赋值然后整体写回缓存。6. 登录授权、收藏、反馈与性能细节收尾6.1 微信小程序登录授权流程的现状处理pages/login/login是商城项目的登录页源码在这块处理了一个关键变化微信在基础库 2.27.1 版本之后wx.getUserProfile接口被收回不再返回真实头像昵称。新版做法是使用button组件的open-typechooseAvatar获取头像配合input[typenickname]获取昵称。源码里的 login 页面如果还是旧版wx.getUserProfile写法在现在的基础库环境下可以正常弹出授权框但在最新版本会拿到匿名数据。这个兼容性细节在跑通项目时经常被忽略表现出来就是头像昵称全是灰色的默认值。// pages/login/login.js 登录跳转后的状态同步 wx.setStorageSync(token, mock_token_ Date.now()); wx.setStorageSync(userInfo, userInfo); wx.navigateBack();登录成功后写入token和userInfo到缓存token是模拟的真实项目这里应该用wx.login拿到的code换取后端的openid和自定义登录态。navigateBack返回上一页上一页的onShow里重新读取缓存就能展示登录状态。如果登录页是通过wx.redirectTo进入的这里navigateBack会失效需要使用wx.switchTab或wx.reLaunch指定落地页。6.2 收藏功能的状态同步机制pages/collect/collect是收藏列表页商品详情页的“收藏”按钮把商品 id 存入collect缓存数组。收藏状态的同步有两个关键时机详情页onLoad时检查当前商品是否在收藏数组里以及点击收藏按钮后更新数组并重新写入缓存。商品是否已收藏的判断用Array.some或includes都可以但收藏数组里如果存的是对象而不是 id 数组就需要用some(item item.id currentId)判断。每次收藏操作后缓存要立刻重写否则退出详情页再进来看不到收藏状态变化。收藏按钮的 UI 在源码里用wxss切换类名实现红心灰心切换本质是通过data里一个布尔字段驱动class的条件渲染。6.3 feedback 反馈页面与 setData 性能优化pages/feedback/feedback页面用于用户提交意见或问题典型结构是文本域 图片上传 提交按钮。文本域的bindinput事件会高频触发setData性能差的手机上如果每次输入都同步整页数据会有明显卡顿。源码里可以改成只在输入框失焦或者点击提交时才读取textarea的值。// pages/feedback/feedback.js 提交时的数据收集 submitFeedback: function () { const content this.data.feedbackContent.trim(); if (!content) { wx.showToast({ title: 请输入反馈内容, icon: none }); return; } const feedbackList wx.getStorageSync(feedbackList) || []; feedbackList.unshift({ id: Date.now(), content, time: new Date().toLocaleString() }); wx.setStorageSync(feedbackList, feedbackList); wx.showToast({ title: 提交成功, icon: success }); setTimeout(() { wx.navigateBack(); }, 1500); }trim()去除首尾空格是必要的输入清洗步骤用户提交全空格内容时不会进入提交逻辑。反馈数据落地到本地缓存刷新后还在这个方案在源码里够用真实项目一般会把反馈提交到后端工单系统。提交成功后用setTimeout延迟返回上一页让用户能看清成功提示这个交互细节体验不错。这里的setTimeout记得在页面onUnload里清理否则页面已经关掉定时器还触发navigateBack会回到一个意想不到的页面。本文还有配套的精品资源点击获取