简介:这是一份仿美团外卖的微信小程序完整前端源码包,面向正在学习小程序开发或准备搭建外卖类项目的开发者,可帮助理解从首页商家展示、菜品分类、下单结算到订单详情、地址管理等核心流程的实现方式。压缩包共210个文件,约577KB,以js逻辑脚本、wxml页面结构、wxss样式表、json配置及png/jpg图片素材为主,目录按页面模块拆分,便于对照学习界面交互与数据绑定。资源已吸引2760人学习下载,适合作为实战参考。通过阅读源码,可掌握微信小程序原生开发中的页面路由、组件复用、本地数据存储和API调用思路,也能了解仿美团外卖场景下搜索、评价、退款、订单地图等功能的模块划分与代码组织方式,对想要独立完成同类型小程序作品或准备面试作品的开发者有一定借鉴价值。
1. 为什么「仿美团外卖微信小程序源码」值得买,但更值得拆开重写
一看到「仿美团外卖微信小程序源码.rar」这类资源,先别急着解压丢进微信开发者工具。市面上流传的源码大多把页面还原做得很像:点餐、购物车、结算界面齐全,但接口地址往往是写死的测试域名,位置是写死的假坐标,用户授权还停留在已被回收的wx.getUserInfo老写法。真正有价值的东西其实在一堆文件里藏着:分类导航、商品列表、购物车联动、订单状态流转这些交互,自己从零搭要折腾两三天,改一份结构清晰的源码却能在一个晚上跑通全部页面。我一般会按工程师会做的思路,从目录结构、商品数据设计、配送与订单模拟、请求封装到上线前的必改坑点,把这套源码从头到尾拆一遍,改完的东西可以直接替换到自己项目里当骨架用。
2. 先把 .rar 里的货盘明白:导入微信开发者工具与目录结构
2.1 用微信开发者工具跑通的最小命令集
Windows 上直接右键解压 .rar 就行,macOS 或 Linux 终端里我习惯用unar,因为微信开发者工具的「导入项目」面板不认压缩包,必须先解压到能看到项目根文件的目录。
# macOS 先装 unar,一次性解压 rar 不乱码 brew install unar unar 仿美团外卖微信小程序源码.rar # 解压后确认项目根文件是否齐全 ls -la ./仿美团外卖微信小程序源码/app.json ls -la ./仿美团外卖微信小程序源码/project.config.json确认app.json和project.config.json都存在后,打开微信开发者工具,点击「导入项目」,目录选到解压出来的项目根路径。AppID 可以先用测试号,不必急着填自己的小程序账号。基础库版本按源码里project.config.json声明的最低版本选,大部分老源码声明 2.x,我一般直接改到当前稳定版本再跑,遇到废弃 API 会当场在控制台看到警告。
导入后第一件事不是点编译,而是检查详情面板里的「本地设置」:
| 配置项 | 建议值 | 原因 |
|---|---|---|
| 调试基础库 | 稳定版最新 | 老代码里wx.getUserInfo等接口行为已变 |
| 不校验合法域名 | 开发期勾选 | 源码默认接口多为http://测试地址 |
| ES6 转 ES5 | 保持默认 | 老源码常混用 async/await 与 callback |
| 增强编译 | 关闭再开一次 | 部分老工程开启后出现 Component 路径解析错误 |
这些配置不影响最终上线包,只决定本地调试能不能少踩哑巴亏。勾完「不校验合法域名」后,接口请求才能从http://127.0.0.1:8080这类开发地址正常发出,真实上线前必须换成https并在小程序后台登记域名。
2.2 目录结构里的四个关键模块
解压后先不要进pages文件夹乱翻代码,先看app.json。这个文件的pages数组顺序决定启动页,数组第一项就是进入小程序后加载的页面,老源码的第一项通常不是首页,而是pages/loading/loading或pages/login/login。
{ "pages": [ "pages/index/index", "pages/cart/cart", "pages/order/order", "pages/mine/mine" ], "window": { "navigationStyle": "custom", "backgroundColor": "#f5f5f5" }, "tabBar": { "color": "#999999", "selectedColor": "#ffae00", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/order/order", "text": "订单" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] } }navigationStyle: "custom"是仿美团类源码最常见的一个坑:导航栏隐藏后,状态栏高度、胶囊按钮位置都要自己在代码里算,否则顶部内容和微信胶囊重叠。我在跑通源码之前,会先把这一项改成默认的default,等页面基本调通再改回自定义导航栏,减少第一轮的调试变量。
app.json之后,按这个顺序读代码:
| 目录或文件 | 职责 | 你最可能需要改的点 |
|---|---|---|
utils/config.js | 接口地址、密钥、公共常量 | 全局替换为真实后端地址 |
components/product-card | 商品卡片组件 | 点击区域、角标样式、加入购物车事件名 |
pages/index | 首页分类与推荐位 | 分类切换逻辑和首屏加载 |
pages/cart | 购物车页 | 购物车本地缓存和数量同步 |
pages/order | 订单列表与详情 | 订单状态枚举和倒计时逻辑 |
老源码的公共代码习惯放在utils/,组件放在components/,页面放在pages/。如果解压后发现所有页面都堆在pages下没有组件目录,说明这份源码基本没有模块化,后续改起来成本会高不少,这时先抽一个商品卡片组件比直接改页面更值。
3. 商品分类与购物车的核心数据设计
3.1 用扁平数组把左侧分类和右侧商品绑起来
仿美团外卖首页最核心的交互,是左侧垂直分类、右侧商品列表,点分类后右侧滚动定位。很多老源码会直接把分类和商品嵌套成一个多层对象,结构好看,但setData的时候要么整页更新,要么深拷贝数组,改动一行数据都会造成明显卡顿。
我通常把分类和商品拆成两个扁平数组,商品里只存categoryId,分类里不存商品数组。
// pages/index/data.js 简化结构 module.exports = { categories: [ { id: 1, name: '热销', icon: '/assets/hot.png' }, { id: 2, name: '主食', icon: '/assets/rice.png' } ], goods: [ { id: 1001, categoryId: 1, name: '招牌黄焖鸡', price: 18.5, sales: 232 }, { id: 1002, categoryId: 2, name: '米饭', price: 2.0, sales: 1024 } ] };这里categoryId是关联字段,商品页只维护一个currentGoods,分类切换时用filter过滤,不把全量数据塞进渲染层。
// pages/index/index.js 分类切换逻辑 const { categories, goods } = require('./data.js'); Page({ data: { categories: [], currentGoods: [], activeCategoryId: 0 }, onLoad() { this.setData({ categories: categories, currentGoods: goods.filter(item => item.categoryId === 1), activeCategoryId: 1 }); }, onCategoryTap(e) { const id = e.currentTarget.dataset.id; // 只过滤当前分类,保留原数据避免二次请求 const currentGoods = goods.filter(item => item.categoryId === id); this.setData({ activeCategoryId: id, currentGoods: currentGoods }); } });e.currentTarget.dataset.id是从 WXML 里>// pages/index/index.js 加入购物车 addToCart(e) { const { id, name, price } = e.currentTarget.dataset; // 先浅拷贝一层,避免直接改 data 造成不可预期渲染 const cart = { ...this.data.cart }; const line = cart[id] || { name: name, price: price, count: 0 }; line.count += 1; cart[id] = line; const totalCount = Object.keys(cart) .reduce((sum, key) => sum + cart[key].count, 0); const totalPrice = this.calcTotalPrice(cart); this.setData({ cart: cart, totalCount: totalCount, totalPrice: totalPrice }); }
这里的关键参数是cart[id]和line.count。cart用对象而不是数组,是为了让setData能通过cart.1001.count这种路径只更新单个字段;line.count的增加必须在拷贝后的对象上完成,否则后续 diff 可能会漏掉数据源变化。计算totalCount时用reduce而不是for循环,语义更清晰,性能在这个数据量级上没有实际差别。
| 字段 | 作用 | 更新频率 |
|---|---|---|
cart | 购物车明细对象,key 为商品 id | 每次点击加号 |
totalCount | tabBar 角标和结算栏数量 | 与 cart 同步 |
totalPrice | 结算金额展示 | 与 cart 同步 |
页面 WXML 里对应的角标要同时监听totalCount,不要自己再维护一个本地变量,否则首页加购、购物车页删减、订单页取消三个入口改完会互相覆盖。老源码最常见的 bug 就是角标只在一个页面里变化,切到其他页面后数量不刷新,原因是角标组件没有从公共数据源取值。
4. 定位、配送费与订单状态:把「美团外卖感」做出来
4.1 用 getLocation 和 chooseLocation 拿到真实经纬度
仿美团外卖的源码里,首页顶部一定会有一个城市名和定位图标。老源码通常直接写死city: '北京市',真机一跑,这个位置永远不变,用户体验一眼假。微信小程序拿用户定位要用wx.getLocation,但这个接口需要用户授权,且在 app.json 里声明隐私接口。
// pages/index/index.js 定位逻辑 Page({ data: { city: '定位中', latitude: 0, longitude: 0 }, onLoad() { wx.getLocation({ type: 'gcj02', // 火星坐标系,美团地图用的是这个 success: (res) => { // 成功后再调接口解析城市名,这里简化成固定返回 getApp().globalData.latitude = res.latitude; getApp().globalData.longitude = res.longitude; this.setData({ latitude: res.latitude, longitude: res.longitude, city: this.mockCityByLocation(res.latitude, res.longitude) }); }, fail: () => { // 授权被拒时退回默认商圈,不能阻断主流程 this.setData({ city: '北京' }); } }); } });type: 'gcj02'很重要,微信返回的原始坐标是 wgs84,国内地图服务和配送范围计算一般用 gcj02,两套坐标系混用会导致定位点在图上偏差几百米。requiredPrivateInfos也要在 app.json 里补上:
{ "requiredPrivateInfos": ["getLocation"] }如果不声明,真机调试时会直接报getLocation:fail api scope is not declared,老源码基本都缺这个配置,属于拿到手必须先补的一步。城市名解析通常调用后端逆地理编码接口,源码里没有就给个 mock 方法,核心是先把经纬度存到globalData,后续算距离、筛门店都用这一份坐标。
4.2 配送费阶梯与订单状态机
配送费是仿美团外卖里绕不开的业务细节。很多源码只用一行totalPrice > 30 ? 0 : 6处理,看起来简单,但门店起送价、配送距离、免配送费门槛都没体现,稍加需求就会重写。我一般把配送费抽成独立函数,放在utils/delivery.js里。
// utils/delivery.js 配送费计算 function calcDeliveryFee(cartAmount, distanceKm) { if (cartAmount >= 30) return 0; // 满 30 免配送费 if (distanceKm <= 2) return 4; // 2 公里内基础配送费 const extra = Math.ceil(distanceKm - 2) * 1.5; // 超出部分每公里加 1.5 return Number((4 + extra).toFixed(1)); } module.exports = { calcDeliveryFee };Math.ceil表示向上取整,3.2 公里按 4 公里算;cartAmount是购物车商品总价,不包含配送费和打包费,这个口径要和结算页保持一致,否则会出现「满 30 免配送」的临界点争议。distanceKm 由门店坐标和用户坐标用 haversine 公式算出,源码里如果已有距离计算函数,确认单位是公里而不是米。
订单状态机建议用常量对象而不是魔法数字:
// utils/order-status.js const ORDER_STATUS = { UNPAID: 0, // 待支付 WAITING: 1, // 商家接单 DELIVERING: 2, // 配送中 DONE: 3, // 已完成 CANCELED: -1 // 已取消 }; function canTransit(from, to) { const nextMap = { [ORDER_STATUS.UNPAID]: [ORDER_STATUS.WAITING, ORDER_STATUS.CANCELED], [ORDER_STATUS.WAITING]: [ORDER_STATUS.DELIVERING, ORDER_STATUS.CANCELED], [ORDER_STATUS.DELIVERING]: [ORDER_STATUS.DONE] }; return (nextMap[from] || []).includes(to); }状态机的好处是防止订单从「配送中」直接跳到「待支付」这种脏数据。nextMap[from]表示当前状态允许转移到的状态集合,includes(to)判断目标状态是否合法。订单列表页的按钮文案、倒计时、取消入口都从这里派生,改动业务规则时只动这一个文件。
| 状态值 | 页面展示文案 | 允许的用户操作 |
|---|---|---|
| 0 | 待支付 | 取消订单、去支付 |
| 1 | 商家接单中 | 无 |
| 2 | 配送中 | 查看配送进度 |
| 3 | 已完成 | 再来一单、评价 |
| -1 | 已取消 | 删除订单 |
5. 把后端请求封装成可统一换源的一层
5.1 用 Promise 包装 wx.request,并把 401 踢回登录
老源码里最常见的是到处wx.request({...}),改一个公共请求参数要全局搜索替换。我会先加一层request封装,所有页面统一调用。
// utils/request.js const { BASE_URL } = require('./config.js'); function request(options) { const token = wx.getStorageSync('token'); 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: `Bearer ${token}` }, success: (res) => { if (res.statusCode === 401) { wx.removeStorageSync('token'); wx.reLaunch({ url: '/pages/login/index' }); reject(res); return; } if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { wx.showToast({ title: '请求失败', icon: 'none' }); reject(res); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };wx.request本身不返回 Promise,这里包一层后页面可以用async/await写请求逻辑,错误处理集中在一个位置。res.statusCode是 HTTP 状态码,业务码通常在res.data.code里,和 HTTP 状态码是两回事,这套封装只处理 HTTP 层,业务码留给各页面判断。
封装后原来的wx.request调用全部替换成下面这种写法:
const { request } = require('../../utils/request.js'); async function fetchGoods() { try { const data = await request({ url: '/goods/list', method: 'GET' }); this.setData({ goods: data.list }); } catch (e) { // 错误已在 request 内统一 toast } }5.2 把写死的域名抽到环境配置,避免每次换后端都翻源码
源码里直接写http://192.168.1.100:8080的情况很普遍,尤其从 .rar 资源拿到手后,作者机器上的内网地址不可能通用。统一放到utils/config.js:
// utils/config.js const ENV = 'dev'; // 上线前改成 prod const CONFIG = { dev: { BASE_URL: 'http://127.0.0.1:8080/api', ENABLE_MOCK: true }, prod: { BASE_URL: 'https://api.yourdomain.com/api', ENABLE_MOCK: false } }; module.exports = CONFIG[ENV];| 配置项 | 开发环境 | 生产环境 |
|---|---|---|
BASE_URL | 本地后端服务 | 已备案并配置在小程序后台的合法域名 |
ENABLE_MOCK | true,不依赖后端 | false,走真实接口 |
PIC_DOMAIN | 本地图片地址 | 云端存储的 CDN 地址 |
改环境只需改一行ENV,不用到处翻文件。需要注意的是,开发环境的http://地址只能在「不校验合法域名」开启时用,生产环境必须换成https且在微信公众平台「开发管理-服务器域名」里登记,否则正式版真机请求会被拦截。用 HBuilderX 跑 uni-app 微信小程序工程时,封装思路也一样,区别只是uni.request本身支持 Promise,可以少包一层。
6. 改加载页、长列表和用户信息之前,先把这三个坑填上
6.1 「修改刚进入的加载页面」的正确位置
老源码会专门放一个pages/loading/loading页面,里面用setTimeout固定等 1.5 秒再跳首页。真机切后台再回来,这个等待会显得非常慢。修改时要把「固定延时」换成「数据预加载完成再跳转」。
// pages/loading/index.js 改造后的加载逻辑 onLoad() { Promise.all([ this.fetchBanners(), // 拉首页轮播图 this.fetchCategories() // 拉分类数据 ]).then(() => { wx.reLaunch({ url: '/pages/index/index' }); }).catch(() => { wx.reLaunch({ url: '/pages/index/index' }); }); }加载页的文案和 logo 图修改在 WXML 里直接替换资源路径,真正要改的是这个跳转时机。Promise.all保证两个接口都返回后再进首页,接口报错也不卡死流程,兜底直接进首页。
6.2 长列表别一页全渲染
商品列表和订单列表的scroll-view下拉时会不断追加数据,但页面data里的数组会越来越大。每次setData传全部数组会把渲染层撑爆,常见做法是分页后只把新数据拼在末尾,并通过wx.pageScrollTo控制滚动位置。源码里如果没有分页逻辑,至少要把onReachBottom的页码参数补上,否则订单多到 50 条之后开始白屏。
6.3 用户信息授权必须换头像昵称填写能力
老源码里的wx.getUserInfo和wx.getUserProfile在最新基础库都已受限,点击按钮直接触发会返回失败。微信官方改成头像昵称填写能力,需要用户在button上主动选择头像和输入昵称,不能静默获取。改造时把原来弹窗授权改成表单式入口:
<button open-type="chooseAvatar" bindchooseavatar="onChooseAvatar"> 选择头像 </button> <input type="nickname" bindblur="onNicknameInput" placeholder="请输入昵称" />open-type="chooseAvatar"只能从按钮触发,type="nickname"的输入框会自动带上微信昵称推荐能力,所以不要想着绕过这个交互去拿用户昵称。改完这三处,这套源码才算能通过开发者工具的真机预览和基础库检查,后续再做网络层替换、UI 自定义才有意义。
本文还有配套的精品资源,点击获取