资讯动态

微信小程序外卖点餐源码拆解:购物车数据流与分页加载实战

发布时间:2026/10/15 11:39:10 来源:尧图企业网站定制
简介这份资源是微信小程序「我厨」的完整源码包面向正在学习小程序开发、需要实战项目练手的初学者与进阶开发者。项目围绕菜谱展示与购物车场景展开涵盖首页推荐、分类浏览、商品详情、购物车及个人中心等典型模块适合用来理解小程序页面组织、组件复用与数据交互的完整流程。压缩包共50个文件以14个wxml页面结构、7个wxss样式、9个js逻辑脚本和7个json配置为主另含11张png图片资源整体约125KB体量轻巧便于快速导入微信开发者工具运行调试。目录中api、utils、pages、images等分层清晰app.js与app.json承担全局逻辑与路由配置读者可据此掌握页面注册、请求封装与静态资源管理的常见做法。目前已有133人学习下载适合作为课程设计、毕业项目或自学练手的参考模板帮助读者在真实代码中理解小程序从结构到交互的落地方式。1. 从一份外卖点餐小程序源码说起我厨能解决什么最近在整理本地积累的开源项目时翻出了一份名为「weixin_wochu-微信小程序我厨.zip」的压缩包。这是一个基于微信小程序原生框架开发的外卖点餐类项目核心场景覆盖了菜品浏览、购物车管理、订单提交这几个高频模块。如果你正在接餐饮类小程序的外包单子或者想找一个结构完整、逻辑清晰的小程序练手项目这份源码的参考价值比较直接——它把点餐流程里最琐碎的状态同步问题处理得相对干净不是那种只摆几个静态页面的空壳 Demo。我拿到压缩包后做的第一件事是解压看目录结构确认它是不是一个能跑起来的完整工程。从文件组织来看项目采用了微信小程序标准的pagesutilscomponents分层方式没有引入第三方 UI 库所有组件都是原生写法。这意味着你不需要额外配置 npm 构建用微信开发者工具直接导入就能预览。适合的人群很明确有一点点小程序基础、想通过一个真实业务场景理解数据流管理的开发者以及需要快速搭出点餐原型的产品或设计同学。2. 拆包看结构目录组织与核心模块职责2.1 工程目录一览与文件职责划分解压后不要急着点编译先花几分钟把目录树过一遍。这个项目的文件命名比较直白没有用缩写或拼音混写对新手比较友好。下面是我整理的关键目录说明目录/文件职责备注app.json全局页面注册、窗口样式、tabBar 配置点餐流程的页面路径都在这里声明app.wxss全局样式与公共类定义了主色调和按钮基础样式pages/index首页菜品分类与列表展示数据从本地 mock 文件读取pages/cart购物车页数量增减与总价计算核心逻辑集中在cart.jspages/order订单确认与提交包含表单校验和提交模拟utils/util.js格式化时间、价格等工具函数被多个页面引用mock/data.js静态菜品数据替换成接口请求的入口在这里从表里能看出来项目把「数据源」和「视图层」做了初步分离mock/data.js是唯一的数据出口。这种设计的好处是当你需要对接真实后端时只需要改这一个文件不用去每个页面里翻请求代码。常见做法是把这个文件替换成api/index.js里面封装wx.request的 Promise 版本然后页面里通过import调用。2.2 页面注册与全局配置的关联逻辑app.json里的pages数组决定了小程序启动时加载哪些页面顺序很重要——数组第一项就是启动页。这个项目把pages/index/index放在首位符合点餐场景「先看菜再下单」的路径。如果你想把购物车作为启动页直接调整数组顺序即可但要注意 tabBar 的pagePath必须与pages中的路径完全一致否则开发者工具会报「tabBar 配置错误」。另一个容易忽略的点是window配置里的navigationBarTitleText。项目在每个页面的json文件里单独覆盖了标题比如购物车页显示「购物车」订单页显示「确认订单」。这种页面级配置优先级高于全局配置排查标题不生效的问题时先看页面自己的json有没有写死。{ pages: [ pages/index/index, pages/cart/cart, pages/order/order ], window: { navigationBarBackgroundColor: #ffffff, navigationBarTitleText: 我厨, navigationBarTextStyle: black }, tabBar: { list: [ { pagePath: pages/index/index, text: 点餐 }, { pagePath: pages/cart/cart, text: 购物车 } ] } }上面这段配置里tabBar只列了两个页面订单页没有出现在 tabBar 中这是合理的——订单确认通常是从购物车跳转过去的不需要常驻入口。参数说明navigationBarBackgroundColor只支持十六进制颜色值写white或#fff在部分基础库版本下会解析异常建议统一用六位十六进制。3. 购物车数据流从点击加购到总价刷新3.1 数据结构设计与状态同步策略购物车是这个项目里逻辑最密的部分。我翻了一遍cart.js发现它没有用globalData存购物车数据而是每个页面独立维护一份cartList通过wx.setStorageSync做本地持久化。这种做法的优点是页面间耦合低缺点是如果首页和购物车页同时打开首页加购后购物车页不会自动刷新需要手动下拉或切换 tab 触发onShow重新读取。数据结构长这样每个购物车项包含id、name、price、count、selected五个字段。selected控制是否勾选结算count控制数量。总价计算时先过滤selected为true的项再累加price * count。这里有个细节价格在 mock 数据里是浮点数直接累加会出现0.1 0.2 0.30000000000000004的经典问题。项目在utils/util.js里写了一个formatPrice函数用toFixed(2)做最终展示但计算过程中没有做整数化处理。如果你的菜品单价有分位建议在计算前先乘以 100 转成整数算完再除回去。// utils/util.js 中的价格格式化函数 function formatPrice(value) { // 先转数字避免字符串拼接 const num Number(value); if (isNaN(num)) { return 0.00; } // 保留两位小数返回字符串 return num.toFixed(2); } // 购物车总价计算逻辑简化版 function calcTotal(cartList) { let total 0; cartList.forEach(item { if (item.selected) { // 单价乘以数量累加 total item.price * item.count; } }); // 最终展示前格式化 return formatPrice(total); }逻辑说明formatPrice只负责展示层格式化不参与中间计算。calcTotal遍历时用forEach而不是for循环可读性更好但注意forEach里不能break如果后续要加「满减」逻辑需要提前终止得换成for或some。参数方面cartList必须是数组如果从 storage 读出来是null要先给默认值[]否则forEach会直接报错。3.2 加购与减购的事件绑定写法首页的加购按钮绑定的是bindtap通过>// pages/cart/cart.js 中的数量增减处理 Page({ data: { cartList: [] }, onShow() { // 每次页面显示时重新读取本地缓存 const list wx.getStorageSync(cartList) || []; this.setData({ cartList: list }); }, // 增加数量 increase(e) { const index e.currentTarget.dataset.index; const list this.data.cartList; // 直接修改数组元素然后整体 setData list[index].count 1; this.setData({ cartList: list }); // 同步回本地缓存 wx.setStorageSync(cartList, list); }, // 减少数量 decrease(e) { const index e.currentTarget.dataset.index; const list this.data.cartList; if (list[index].count 1) { // 数量为 1 时再减询问是否删除 wx.showModal({ title: 提示, content: 确定要移除这道菜吗, success: (res) { if (res.confirm) { list.splice(index, 1); this.setData({ cartList: list }); wx.setStorageSync(cartList, list); } } }); return; } list[index].count - 1; this.setData({ cartList: list }); wx.setStorageSync(cartList, list); } });逻辑说明onShow里重新读缓存是为了解决前面提到的页面间不同步问题。increase和decrease都直接修改了this.data.cartList的引用然后整体setData。这种写法在小数据量下没问题但如果购物车有几十项每次setData整个数组会有性能损耗。优化方向是用setData的路径写法比如this.setData({ [cartList[ index ].count]: newCount })只更新变化的那一项。参数上e.currentTarget.dataset.index拿到的是字符串做加减运算前最好用Number()转一下虽然 JS 会隐式转换但显式写出来更稳。4. 避坑与排查导入编译到真机预览的五个常见问题4.1 开发者工具导入后报「app.json 未找到」现象解压后直接点「导入项目」工具提示找不到app.json页面一片空白。原因通常是压缩包多套了一层文件夹比如解压出来是weixin_wochu/weixin_wochu/app.json而导入时选的是外层目录。解决方法是进到真正包含app.json的那一层再导入或者把内层文件全部剪切到外层。我一般会先看目录里有没有project.config.json有的话以它为锚点找同级app.json。4.2 模拟器能跑真机预览白屏现象开发者工具里一切正常扫码真机预览时白屏或卡在启动页。原因多半是app.json里用了真机不支持的基础库特性或者wxss里用了rpx之外的单位导致布局溢出。排查步骤先在工具里点「真机调试」看控制台有没有报错如果没有报错检查app.json的style: v2是否开启部分老项目在 v2 样式下会布局错乱。解决方法是把style字段去掉回退到默认样式。4.3 购物车数量变化后总价不更新现象点击加减按钮数量变了但底部总价纹丝不动。原因通常是总价没有在setData里一起更新或者总价是在onLoad里算了一次就再也没动过。解决方法是把总价计算封装成独立函数在每次cartList变化后调用并把结果一并setData。注意不要在onShow里只更新列表不更新总价两者必须同步。4.4 本地缓存数据格式错乱导致页面崩溃现象某次加购后再进购物车页直接报Cannot read property forEach of null。原因是wx.getStorageSync读出来的数据被手动改过或者之前存的是对象而不是数组。解决方法是读取后加一层类型判断const list Array.isArray(raw) ? raw : []。另外在开发阶段可以加一个「清空缓存」的隐藏按钮方便快速重置状态。4.5 订单提交后返回购物车旧数据还在现象提交订单后跳回购物车发现之前选的菜还在列表里。原因是订单提交成功后没有清空本地缓存和页面数据。解决方法是在order.js的提交成功回调里执行wx.removeStorageSync(cartList)同时用getCurrentPages()拿到购物车页实例手动调它的setData({ cartList: [] })。这一步容易被漏掉因为订单页和购物车页是两个独立的 Page 实例不会自动联动。5. 从 mock 到接口替换数据源与分页加载的实操5.1 把静态数据文件改成请求封装项目默认从mock/data.js读菜品列表这个文件导出了一个数组。要对接真实后端第一步是新建api/index.js把wx.request包成 Promise然后逐个替换页面里的import。下面是一个通用的请求封装写法// api/index.js const BASE_URL https://your-domain.com/api; 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 }, success: (res) { // 假设后端返回格式为 { code, data, msg } if (res.data.code 0) { resolve(res.data.data); } else { // 业务错误统一提示 wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) { // 网络错误 wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } // 获取菜品列表 function getDishList(params) { return request({ url: /dish/list, data: params }); } module.exports { getDishList };逻辑说明request函数把wx.request的回调风格转成 Promise页面里就能用async/await或.then调用。参数上BASE_URL要换成你自己的接口域名且必须在小程序后台配置「request 合法域名」否则真机请求会被拦截。header里的content-type按后端要求调整如果是表单提交要改成application/x-www-form-urlencoded。5.2 列表分页与下拉加载的实现要点菜品列表如果数据量大一次性渲染会卡顿。常见做法是分页加载每页 10 到 20 条。在index.js里维护page和hasMore两个状态onReachBottom触发下一页请求。注意onReachBottom在页面没有滚动条时不会触发所以列表容器必须有固定高度或内容超出屏幕。另外请求过程中要加loading锁防止用户快速上拉导致重复请求同一页。// pages/index/index.js 分页逻辑片段 Page({ data: { dishList: [], page: 1, hasMore: true, loading: false }, onLoad() { this.loadDishList(); }, onReachBottom() { if (!this.data.hasMore || this.data.loading) { return; } this.loadDishList(); }, async loadDishList() { this.setData({ loading: true }); try { const res await api.getDishList({ page: this.data.page, size: 10 }); const newList this.data.dishList.concat(res.list); this.setData({ dishList: newList, page: this.data.page 1, hasMore: res.hasMore, loading: false }); } catch (e) { this.setData({ loading: false }); } } });逻辑说明onReachBottom里先判断hasMore和loading避免无效请求。loadDishList用async/await写法try/catch捕获请求异常。参数上page从 1 开始每次请求成功后加 1hasMore由后端返回前端不要自己猜。如果后端没有返回hasMore可以用「返回条数小于 pageSize」来判断是否还有下一页。5.3 真机验证与性能面板的使用改完数据源后不要只在模拟器里看。点开发者工具右上角的「真机调试」用手机扫码重点看两个地方一是 Network 面板里请求耗时如果单个接口超过 500ms要考虑加缓存或骨架屏二是 Performance 面板的 FPS 曲线滑动列表时如果掉到 30 以下说明渲染压力大需要减少setData的数据量或使用虚拟列表。我一般会在loadDishList成功后打一个console.time和console.timeEnd粗略估算从请求到渲染完成的耗时这个习惯帮我提前发现过好几次接口慢查询的问题。从那以后我每次拿到一份小程序源码都强制走一遍「导入 → 真机预览 → 改数据源 → 分页压测」的流程不跳过任何一步。希望这份拆解能帮到你少走一些我踩过的弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑