资讯动态

微信小程序外卖源码解析:从类目联动到购物车金额计算

发布时间:2026/9/15 13:56:54 来源:尧图企业网站定制
简介这是一份微信小程序外卖点餐项目“通乐居外卖”的完整源代码与界面截图面向小程序开发者、在校学生或想快速搭建外卖类demo的初学者可用于学习小程序前端架构、组件通信与页面交互实现。压缩包共254个文件约1.18MB包含61个js逻辑文件、59个wxml页面结构、46个wxss样式以及png/jpg截图和json配置等源码目录清晰便于按模块查阅修改。已有453人学习下载适合需要参考完整项目结构并快速上手的开发者。资源提供全套源码和界面截图覆盖商品分类、菜品列表、购物车、订单提交等外卖核心流程既能帮助理解从页面渲染到数据绑定的完整代码路径也可作为课程设计或毕业设计的二次开发基础。另外源码中页面与样式分离WXML与WXSS配合使用的方式对初学者熟悉小程序布局也有直接帮助。1. 外卖小程序的源码包拿到手先看哪里外卖小程序的源码包拿到手最常见的情形是几十个文件铺在资源管理器里app.js、pages、images 混在一起想找改菜价的地方只能一个个点开。通乐居外卖这份源代码的价值不在于它有多少高级组件而是把外卖应用最核心的三段链路——类目切换、加购结算、订单提交——用原生微信小程序语法完整走了一遍还附带 dish-1.jpg 到 dish-9.jpg 的菜品示例图跑起来就能看到完整效果。对正在做微信小程序课程设计或毕业设计的读者这份源码适合作为骨架改造对想快速上手微信小程序外卖开发的读者这篇文章会把数据流、金额计算和资源引用的细节逐层拆开。2. 通乐居外卖的页面骨架与类目菜品联动打开开发者工具导入项目目录之后先不要急着预览把 app.json 完整读一遍。微信小程序的项目实例里app.json 就是整个应用的地图哪些页面被注册、tabBar 有几个入口、窗口标题是什么全在这里。通乐居外卖沿用了外卖类目最常见的一页三 Tab 结构主页面承担菜品展示和购物车另外两个页面分别处理订单和用户信息。2.1 从 app.json 读出一页三 Tab 的结构pages 数组的第一项是首页tabBar 里的 pagePath 必须与 pages 中注册的路径一一对应。通乐居外卖的典型结构如下表所示页面路径页面职责关键能力pages/index/index菜品列表、类目切换、购物车scroll-view 联动、加购/减购、金额计算pages/order/order订单列表、订单状态展示wx.request 拉取订单、下拉刷新pages/mine/mine用户昵称、头像、登录入口wx.login 换取登录态这三个页面把外卖 C 端用户的主路径覆盖了。实际改造时最常动的是 pages/index/index因为菜品展示和购物车都在这一页商品数据也集中在这里。如果要把通乐居外卖扩展成多人点餐或商家端通常也是从这个页面里把购物车逻辑抽出去单独做成一个组件。{ pages: [ pages/index/index, pages/order/order, pages/mine/mine ], window: { navigationBarTitleText: 通乐居外卖, navigationBarBackgroundColor: #ff6633, navigationBarTextStyle: white }, tabBar: { color: #888888, selectedColor: #ff6633, list: [ { pagePath: pages/index/index, text: 点餐 }, { pagePath: pages/order/order, text: 订单 }, { pagePath: pages/mine/mine, text: 我的 } ] } }这里的 navigationBarBackgroundColor 决定导航栏颜色外卖类应用多用暖色刺激食欲tabBar 的 list 数组最多配五个text 字段不要超过四个汉字否则真机上会被压缩。微信小程序顶部导航栏高度在不同机型上有差异如果后续要自己做自定义导航建议用 wx.getMenuButtonBoundingClientRect 拿到胶囊按钮位置再算高度不要写死 64px。2.2 左侧类目右侧菜品的 scroll-view 联动外卖点餐页面最常见的布局是左侧窄栏放分类右侧宽栏放菜品。实现方式不是用页面滚动而是两个并排的 scroll-view。scroll-view 必须显式给高度否则内容会把页面撑开整个页面滚动而不是局部滚动这是新手最容易踩的第一个坑。view classpage-body scroll-view scroll-y classcategory-panel view wx:for{{categories}} wx:keyid classcategory-item {{activeCategory item.id ? active : }} >onCategoryTap(e) { const targetGroup this.data.dishGroups.find( g g.categoryId e.currentTarget.dataset.id ); if (!targetGroup) return; const firstDishId targetGroup.dishes[0].id; this.setData({ activeCategory: targetGroup.categoryId, targetDishId: dish- firstDishId }); }dataset 取到的是 categoryIdfind 之后拿该分组第一个菜品的 id。这里有一个细节scroll-into-view 的目标必须已经在页面渲染完成如果菜品数据是异步加载的点击时可能找不到元素需要在 setData 回调里再赋值一次或者把 dishGroups 提前注入到 data 里。模拟器上滚不动时优先检查 .dish-panel 有没有 height用百分比高度时父容器也要有高度。2.3 菜品数据绑定与 dish-1.jpg 图片的引用方式通乐居源码包里的 dish-1.jpg 到 dish-9.jpg 是菜品示例图实际开发中建议集中放在 assets/img 目录数据文件里只存文件名。这样做的好处是图片 CDN 化时只改拼接逻辑不动页面结构。菜品数据通常写成独立模块// data/dishes.js module.exports [ { id: 1, name: 招牌回锅肉, price: 2800, image: dish-1.jpg, categoryId: 1001 }, { id: 2, name: 麻婆豆腐, price: 1800, image: dish-2.jpg, categoryId: 1001 } ];价格用“分”为单位存储展示时再除以 100。原因是 JavaScript 的浮点运算在金额场景下不可靠0.1 0.2 不等于 0.3 的问题迟早会在结算时暴露。Category 和 dish 的对应关系可以预先把 dishGroups 按 categoryId 分好组页面渲染时直接遍历分组而不是在 wxml 里做复杂筛选减少模板里的运算量。提示image 组件的 src 写成 /assets/img/dish-1.jpg 时斜杠开头表示项目根目录相对路径真机和模拟器都认。如果写成 assets/img/dish-1.jpg在部分基础库版本里会因为相对路径解析基准不同而 404。3. 购物车数量状态与订单金额计算购物车是外卖小程序里最容易写乱的部分。通乐居外卖的购物车逻辑集中在首页核心是一个对象类型的 cartItemskey 是菜品 idvalue 是数量和勾选状态。选对象而不是数组是因为加购时要按 id 快速查找避免每次 O(n) 遍历而且 setData 支持路径更新只 diff 变化的那一项。3.1 购物车数据模型为什么用对象而不是数组cartItems 初始值是一个空对象每次加购时修改对应 keydata: { cartItems: {}, // { 1: { count: 2, checked: true } } dishes: [], shopId: 10001 }用数组当然也可以但外卖场景下同一道菜反复加减数组的 findIndex 和 splice 会让代码变长。对象模型下加购、减购、清空都只需要定位一个 key。checked 字段用于支持“只结算勾选的菜品”这也和外卖平台的实际交互一致。3.2 加购、减购与清空购物车的 setData 路径写法原生小程序 setData 支持路径字符串可以直接更新深层属性写法是模板字符串加计算属性名addToCart(e) { const dish e.currentTarget.dataset.dish; const key cartItems.${dish.id}; const old this.data.cartItems[dish.id] || { count: 0, checked: true }; this.setData({ [key]: { ...old, count: old.count 1 } }); }这里不能直接 this.data.cartItems[dish.id].count 1 再整体 setData那样会触发整页 diff菜品多时明显卡顿。按路径更新只通知视图层这一个节点变化。e.currentTarget.dataset 里拿到的 dish 是完整对象因为 wxml 里写了>decreaseCart(e) { const dishId e.currentTarget.dataset.id; const current this.data.cartItems[dishId]; if (!current) return; if (current.count 1) { const next { ...this.data.cartItems }; delete next[dishId]; this.setData({ cartItems: next }); } else { this.setData({ [cartItems.${dishId}.count]: current.count - 1 }); } }逻辑说明count 等于 1 时再点减号直接删除该项否则只更新 count 字段。setData 路径支持到第二层属性cartItems.1.count 这种写法在模拟器和真机上都能工作。如果你平时写 uniapp 微信小程序这里的 setData 对应 this.$set 或者直接 this.cartItems next但路径更新的性能优势在 uniapp 里不如原生明显。3.3 金额用“分”为单位计算并提交订单购物车底部要实时算总价通乐居的做法是写一个 calcTotal 方法在每次加购、减购、勾选变化后调用结果存到 data 里供底部结算栏绑定。这样避免在 wxml 里写复杂表达式calcTotal() { const { cartItems, dishes } this.data; let totalFee 0; let totalCount 0; Object.keys(cartItems).forEach((dishId) { const item cartItems[dishId]; if (!item.checked || item.count 0) return; const dish dishes.find(d d.id Number(dishId)); if (dish) { totalFee dish.price * item.count; totalCount item.count; } }); this.setData({ totalFee, // 单位分 totalCount }); }计算时把字符串形式的 dishId 转成 Number是因为 data 里的 id 是数字object 的 key 一定是字符串直接比较会漏算。totalFee 以分为单位返回展示层用 totalFee / 100 保留两位小数。提交订单前需要先保证用户已登录。微信小程序登录的标准流程是 wx.login 拿临时 code交给后端换 openid 和 session_key后端返回自定义 token 存在 Storage 里loginAndSubmit() { wx.login({ success: (res) { wx.request({ url: https://openapi.example.com/auth/login, method: POST, data: { code: res.code }, success: (r) { wx.setStorageSync(token, r.data.token); this.submitOrder(); } }); } }); }code 有效期只有几分钟而且只能用一次所以每次登录都重新调 wx.login不要缓存 code。token 存到 Storage 后后续订单接口带着 token 走。这里 wx.request 的 url 必须配置在公众平台的 request 合法域名里开发阶段可以在开发者工具里勾选“不校验合法域名”上线前必须换成 https 且备案过的域名。订单提交的请求体要明确shopId、总金额、菜品明细。金额不能只传页面计算的 totalFee后端要按菜品单价重新算一遍防止接口被篡改submitOrder() { const { totalFee, totalCount } this.data; if (totalCount 0) { wx.showToast({ title: 购物车是空的, icon: none }); return; } wx.request({ url: https://openapi.example.com/order/create, method: POST, header: { content-type: application/json }, data: { token: wx.getStorageSync(token), shopId: this.data.shopId, totalFee, items: Object.keys(this.data.cartItems) .filter(id this.data.cartItems[id].checked) .map(id ({ dishId: Number(id), count: this.data.cartItems[id].count })) }, success: (res) { if (res.data res.data.code 0) { wx.showToast({ title: 下单成功 }); this.setData({ cartItems: {}, totalFee: 0, totalCount: 0 }); } else { wx.showToast({ title: res.data.message, icon: none }); } }, fail: (err) { console.error(submitOrder failed, err); } }); }method 默认是 GET提交订单必须显式写成 POST。header 里的 content-type 用 application/json这样 data 对象会被序列化成 JSON 字符串后端用 RequestBody 接收即可。过滤掉未勾选的菜品用 filter 加 map不要用 forEach 手动 push代码更简洁。4. 源码包里的图片资源、dll 文件与常见编译排错通乐居外卖源码包里除了小程序代码还能看到 dish-1.jpg 到 dish-9.jpg 的菜品图、.DS_Store 文件以及一个 dll 文件。这些东西有的要规范使用有的可以直接清理搞清楚了才不会在导入项目时被干扰。4.1 菜品截图资源的存放与引用两种方式dish-1.jpg 这一组图片在源码包里直接放在根目录这在小程序里不是好习惯。根目录会被开发者工具当作代码目录整体扫描图片多了会影响编译速度。建议移到 assets/img 下并统一命名。图片引用方式对比引用方式写法示例适用场景本地绝对路径/assets/img/dish-1.jpg菜品少、没配 CDN 之前CDN 完整地址https://cdn.example.com/dish-1.jpg上线后、图片量大代码动态拼接imgSrc(item.image)环境切换、多商户本地绝对路径是最容易排查的方式编译后图片会随包上传缺点是主包体积会膨胀。CDN 方式要把域名加到 downloadFile 合法域名。动态拼接适合通乐居这种以对象存储为后端的项目一个方法统一控制前缀。4.2 .DS_Store 与 dll 文件要不要删.DS_Store 是 macOS 为每个目录生成的索引文件Windows 上完全无用Linux 下还会干扰 diff直接删掉。dll 是 Windows 动态链接库和小程序的后端服务或桌面工具相关和微信小程序前端 JS 运行时没有任何关系通常是打包资源时误塞进来的一并清理# 在项目根目录执行清理源码包中的无关文件 find . -name .DS_Store -type f -delete rm -f *.dllfind 的 -name 匹配文件名-type f 限定只处理普通文件避免误删目录-delete 是删除动作。rm -f 里的 -f 表示文件不存在时不报错。执行完这两条项目目录干净很多。建议顺手在项目根目录加一个 .gitignore把 .DS_Store、*.dll、node_modules 都忽略掉提交源代码时不会把无关文件带进仓库。4.3 编译报错、图片 404 与真机预览的检查顺序导入通乐居源码后如果遇到问题按下面的顺序排查效率最高。第一看开发者工具 Console 里的报错红色错误几乎都会指出文件和行号第二看 Network 面板里图片和 wx.request 的请求状态第三再回到代码里对照路径。现象可能原因处理方式图片 404src 写成了相对路径或大小写不对改成 /assets/img/dish-1.jpg注意 img 目录名小写wx.request 报 errno 600001域名未加入 request 合法域名开发阶段勾选不校验合法域名上线前在 mp 后台配置scroll-view 滚不动没有设置固定高度给 scroll-view 加 height或父容器用 flex 布局模拟器正常真机白屏使用了低版本基础库 API在 app.json 里确认最低基础库版本或替换 API真机预览前点击开发者工具右上角的“预览”按钮生成二维码用微信扫码。如果首页数据没渲染出来优先在真机上打开调试模式看 Console真机报错往往和模拟器不同比如域名校验、图片防盗链、localStorage 不可用等。Network 面板在排查图片路径时特别有用鼠标悬停请求可以看到完整的 URL一眼就能看出是路径少了目录还是域名被拦。5. 落地技巧把单页外卖源码改成分包加载的结构通乐居外卖的页面不多全部塞进主包也能跑但一旦加入菜品详情页、结算页、收货地址页主包体积很容易超过 2MB 的限制。把不常用的页面拆进分包是上线前必做的一步。5.1 subpackages 配置与页面迁移在 app.json 里增加 subpackages 字段把结算页和菜品详情页从主包里移出去{ pages: [ pages/index/index, pages/order/order, pages/mine/mine ], subpackages: [ { root: packageDish, pages: [ pages/dish-detail/dish-detail, pages/checkout/checkout ] } ], preloadRule: { pages/index/index: { network: wifi, packages: [packageDish] } } }分包配置的关键限制是tabBar 页面必须在主包里不能放进分包。所以 index、order、mine 三个页面留在 pages 根部detail 和 checkout 移到 packageDish 下。preloadRule 的作用是用户停留在首页且处于 wifi 环境时预先下载 packageDish 分包点进详情页时不用等待加载。network 字段除了 wifi还可以配 all。迁移后原来从首页跳详情页的 wx.navigateTo 路径要改成 /packageDish/pages/dish-detail/dish-detail相对路径会找不到页面。分包里的页面如果需要共享组件或工具函数放到分包根目录下单独建 components 和 utils 目录主包里的文件分包不能直接 require只能通过插件或公共包方式共享。5.2 图片资源命名规则直接当 CDN key 用通乐居的 dish-1.jpg 这种纯数字命名其实很适合做 CDN key文件名在商户维度内唯一带上目录前缀就是完整的对象存储路径。日常开发中我一般会写一个工具函数统一拼图片地址// utils/img.js const CDN_BASE https://cdn.example.com/tongleju/; function imgSrc(name) { if (!name) return ; if (/^https?:\/\//.test(name)) return name; return CDN_BASE name; } module.exports { imgSrc };正则判断已经传进来的地址是不是完整 URL是就直接返回避免二次拼接。页面里调用时写image src{{imgSrc(item.image)}}或者在 Page 的 methods 里挂载 imgSrc 方法。当图片切到另一个 CDN 或迁移到对象存储新桶时只需要改 CDN_BASE 一个常量所有页面的图片引用自动切换。dish-1.jpg 这类命名本身就是图片的唯一 keyCDN 上原样放一份切换环境只改 imgSrc 的 base页面结构不用动。本文还有配套的精品资源点击获取

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

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

免费获取报价