微信小程序外卖源码解析:从类目联动到购物车金额计算
2026/9/15 13:56:47 网站建设 项目流程

简介:这是一份微信小程序外卖点餐项目“通乐居外卖”的完整源代码与界面截图,面向小程序开发者、在校学生或想快速搭建外卖类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 class="page-body"> <scroll-view scroll-y class="category-panel"> <view wx:for="{{categories}}" wx:key="id" class="category-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 取到的是 categoryId,find 之后拿该分组第一个菜品的 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. 购物车数量状态与订单金额计算

购物车是外卖小程序里最容易写乱的部分。通乐居外卖的购物车逻辑集中在首页,核心是一个对象类型的 cartItems,key 是菜品 id,value 是数量和勾选状态。选对象而不是数组,是因为加购时要按 id 快速查找,避免每次 O(n) 遍历;而且 setData 支持路径更新,只 diff 变化的那一项。

3.1 购物车数据模型为什么用对象而不是数组

cartItems 初始值是一个空对象,每次加购时修改对应 key:

data: { 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 *.dll

find 的 -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 这类命名本身就是图片的唯一 key,CDN 上原样放一份,切换环境只改 imgSrc 的 base,页面结构不用动。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询