
简介这是一份仿美团外卖的微信小程序完整前端源码包面向正在学习小程序开发或准备搭建外卖类项目的开发者可帮助理解从首页商家展示、菜品分类、下单结算到订单详情、地址管理等核心流程的实现方式。压缩包共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每次点击加号totalCounttabBar 角标和结算栏数量与 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_MOCKtrue不依赖后端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-typechooseAvatar bindchooseavataronChooseAvatar 选择头像 /button input typenickname bindbluronNicknameInput placeholder请输入昵称 /open-typechooseAvatar只能从按钮触发typenickname的输入框会自动带上微信昵称推荐能力所以不要想着绕过这个交互去拿用户昵称。改完这三处这套源码才算能通过开发者工具的真机预览和基础库检查后续再做网络层替换、UI 自定义才有意义。本文还有配套的精品资源点击获取