ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用uniapp打造LBS美食小程序:地图找店、点评与搜索推荐全攻略

用uniapp打造LBS美食小程序:地图找店、点评与搜索推荐全攻略 我前后做过三四个吃喝玩乐类的小程序但真正让我觉得“有点大众点评那味儿”的还是最近这个主打美食场景的版本。你要说它是大众点评的完整复刻那肯定谈不上但把“找餐厅、看评分、刷榜单、导航到店”这条主链路跑通再把地图找店、点评信息流、搜索推荐这几个核心模块做扎实用户日常找饭吃的需求是完全能接住的。这篇文章我会把整个项目的拆解思路、技术选型、关键代码、以及从开发到上架过程中踩过的坑全部整理出来。适合正在做小程序但不知道怎么组织功能的开发者也适合想用 uniapp 快速落地一款 LBS 美食应用的朋友参考。我尽量把每一步为什么这么做讲清楚而不是甩一段代码让你自己猜。1. 先想清楚再动手需求拆解与技术选型1.1 这类美食小程序到底在解决什么问题做产品之前先别急着写代码你得分清楚用户打开这个小程序是为了什么。大众点评这类产品能立住核心不是榜单多好看而是它帮用户解决了三个问题附近有什么可吃的、这家店到底行不行、怎么去。其中“附近有什么”是 LBS 场景的刚需用户在外面打开小程序第一眼就想看到以自己为中心、周边一公里内有哪些餐厅评分多少、人均多少、距离多远。这决定了你的首页必须有一张地图或者一个列表按距离排序。“这家店行不行”靠的是点评内容。真实用户的图文评价、评分分布、人均消费这些信息能帮用户快速做决策。别小看这个模块点评内容的数量和质量直接决定了用户愿不愿意二次打开你的小程序。“怎么去”就是把路线接起来。小程序里可以调起导航用户看完店铺页一键跳到地图 App 或者直接在 webview 里展示路线。这个功能看似简单但转化效果极高很多用户就是冲着“看完能直接导航”才留下来的。基于这三个核心需求功能优先级就出来了店铺列表和地图模块排第一点评与评分体系排第二搜索和榜单排第三最后再做个人中心和收藏。没有这些需求打底你堆再多花哨功能都是白搭。1.2 为什么选 uniapp 而不是原生开发开发小程序一般有三条路微信原生、uniapp、纯 H5 套壳。我这次选的是 uniapp不是因为它完美而是因为它在这个项目上确实最合适。先看原生开发。微信原生小程序语法稳定性能和兼容性都没得挑工具链也成熟。但问题也很明显你写了一套只能在微信跑的代码后面想上支付宝、抖音小程序甚至打包成 App全得重来。这个项目我不确定以后会不会出 App 版所以一开始就排除纯原生。再看纯 H5 套壳。开发速度快是真的但小程序里的地图、定位、支付这些能力H5 调用起来很别扭。微信 JSSDK 的定位权限现在收得很紧稍不注意就弹不出来授权框用户体验很糟。而且 H5 在 iOS 上的滚动卡顿、缓存问题调试起来很折磨人。uniapp 的优势在于“一套代码多端编译”。同样是写一套 vue 语法可以编译成微信小程序、H5、App。而且它对地图、支付、定位这些常用能力做了统一封装接口风格一致换平台不用改业务代码只改配置。我个人体验下来从微信小程序切到 H5 调试代码复用率至少在八成以上。这里顺便回应一个经常被问到的问题uniapp 开发的和原生开发的微信小程序到底有没有性能差距。我实测下来普通页面基本感知不到差异只有在大列表滚动、复杂动画这些场景原生会更顺滑一点。但美食点评类应用核心是图和文不是游戏级动画uniapp 完全扛得住。你要是做小游戏那还是老老实实走原生或者专用游戏引擎别拿 uniapp 硬顶。1.3 服务端怎么配云开发还是自建后端这个小程序要有店铺数据、点评内容和用户收藏所以必须有服务端。我这一版用的是微信云开发不是说它多先进而是它在项目早期特别省事。云开发的好处是免鉴权、免运维数据库和存储都是微信侧直接集成。你不需要自己买服务器、配域名、配 HTTPS 证书前端直接调用云函数就能读写数据。我拿家里的一台旧电脑试过自建后端能跑但部署和公网访问的问题很折腾光是一个合法域名备案就能耗掉你好几天。所以个人项目或者早期原型别犹豫直接用云开发。当然云开发不是没有上限。免费额度用完以后要付费数据库并发和存储量对大型应用来说也不太够。但你现在要做的是验证产品模型先把功能跑通再说等用户量真起来了再迁到自建后端也不迟。很多人一上来就铺服务器、整微服务结果写了两个月还在配环境产品影子都没见着。2. 核心功能模块这样设计最顺手2.1 地图找店从定位授权到 POI 标注实现地图模块是美食类小程序的灵魂。我用的地图是腾讯位置服务因为它和微信生态贴合最紧定位和地图 SDK 可以直接在微信小程序里跑。你在公众平台申请 key 的时候记得勾选“微信小程序”选项Web 端和客户端的 key 是不能通用的。先说定位流程。用户进入首页时先通过uni.getLocation获取经纬度这个接口会触发微信自带的授权弹窗。有一点必须注意用户点击拒绝以后你不会拿到任何回调里的坐标所以界面上要做一个“重新授权”的引导按钮。别试图偷偷调wx.getSetting反复弹窗微信对这个管控很严很容易被投诉。拿到经纬度之后下一步就是拉取附近餐厅列表。这里不要自己写“查表算距离”的 SQL效率太低。我用的方案是把餐厅坐标存在数据库里用腾讯位置服务的 WebService API 做周边检索它支持按半径、按关键词、按分类筛选返回结果里直接带距离和评分省去了自己计算的麻烦。前端拿到 POI 列表后用地图组件的markers属性渲染标注点。每个 marker 的callout里显示店名和人均价格点击后跳转店铺详情。这里有个细节marker 的id一定要设置成店铺 ID点 marker 时触发onMarkerTap事件e.detail.markerId才能取到对应的店铺。我第一次就是这个没设置好点击事件一直拿不到数据排查了半天才发现是id丢了个字符串转换。地图还有个坑是坐标偏移。小程序uni.getLocation默认返回的是国测局坐标腾讯位置服务认的也是国测局坐标两者直接匹配没问题。但如果你用了高德地图的数据那就要做坐标转换否则会出现几百米的偏移用户以为在看附近的店实际定位差了半条街。这个坑在接第三方数据源时很容易踩提醒一下。2.2 美食榜单与点评信息流设计榜单模块是让用户“愿意留下来刷”的关键。我做的是三个分类人气榜、好评榜、新店榜。每个榜单是一个独立的云函数接口按不同的字段排序。人气榜按 30 天浏览数和收藏数加权好评榜按评分字段倒序新店榜按创建时间倒序。这个排序逻辑看着简单里面有个容易忽略的点数据量大了以后直接在数据库里orderBy会越用越慢。我当时是把榜单结果做了一层缓存每 10 分钟重新生成一次放到云开发的缓存集合里。用户请求榜单时直接读缓存不查原始表。这样响应速度和数据库压力都得到了优化榜单里顺手加个“更新于 xx 分钟前”的时间戳用户也感知不到数据延迟。点评信息流我放在店铺详情页和首页信息流两个位置。店铺详情页的点评列表按时间倒序就行不需要复杂策略。首页的信息流则要按“距离评分点评数”做一个综合权重排序相当于一个轻量推荐系统。这里我给个小建议别一上来就学大厂的个性化推荐算法用户量几百人的时候按距离和评分排序就是最好的推荐等数据积累到几万条再考虑用户偏好也不迟。点评内容的渲染有个坑是图片懒加载。一条点评可能带四五张图如果一次性全渲染页面内存直接爆炸。我用的是lazy-load属性加图片懒加载列表滚动时再加载视口附近的图片实测内存占用降了一半以上。再加上virtual-list虚拟列表插件来处理长列表滚动流畅度明显提升。不过真实点评不是那么好拿的。冷启动阶段没有用户点评数据从哪里来是个现实问题。我当时的做法是先人工录入一批种子店铺和种子点评把店铺详情做厚至少用户点进来不觉得空旷。然后通过引导用户发表“首条点评”送积分或者小优惠慢慢把内容养起来。没有这一步你的信息流就是空的再好的排序算法也白搭。2.3 搜索与附近推荐的一个小技巧搜索模块我没用全文检索引擎因为美食搜索的 query 通常很短比如“火锅”“烤肉”“川菜”关键词匹配完全够用。我在店铺表里加了一个keywords字段存的是“店名分类标签”的拼接字符串用户搜索时做模糊匹配逻辑简单且性能足够。真正需要花心思的是搜索建议列表。用户输入“火”你应该能联想到“火锅”“火炙寿司”这些热门词。这个联想词表我放在了本地缓存里每次搜索前先去本地表查没有再请求云端。这样既省流量又让联想结果几乎无延迟地弹出。云端的联想词数据每周更新一次来源是搜索日志里高频的失败词和热门词统计。附近推荐是首页信息流里的默认 tab。用户不搜索、不筛选时展示的就是附近一公里内综合评分最高的餐厅列表。这个列表还加了两个维度营业状态营业中/休息中和排队情况如果有排队数据的话。营业状态这个字段很多开发者会忽略但实际体验差异很大用户饭点打开看到一排“休息中”的店体验极差。哪怕做不到实时更新也要按时间段预设一个默认营业状态。3. 关键代码片段与运行细节3.1 统一请求封装拦截器、缓存与错误处理小程序里的网络请求是高频操作必须做一个统一的请求封装。我之前见过很多人每页都直接写uni.request那一改域名或者加个 token 校验几十个页面全要改纯纯的灾难。这版我用了一个request.js封装了uni.request统一处理三件事请求头注入、响应码拦截、缓存策略。// utils/request.js const BASE_URL https://your-api.example.com export function request({ url, method GET, data {}, needAuth true, cache false }) { return new Promise((resolve, reject) { // 缓存命中直接返回 if (cache method GET) { const cacheKey ${url}_${JSON.stringify(data)} const cached uni.getStorageSync(cacheKey) const cacheTime uni.getStorageSync(${cacheKey}_time) if (cached cacheTime Date.now() - cacheTime 5 * 60 * 1000) { return resolve(cached) } } const token uni.getStorageSync(token) uni.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, ...(needAuth token ? { Authorization: Bearer ${token} } : {}) }, timeout: 10000, success: (res) { if (res.statusCode 200) { if (cache method GET) { const cacheKey ${url}_${JSON.stringify(data)} uni.setStorageSync(cacheKey, res.data) uni.setStorageSync(${cacheKey}_time, Date.now()) } resolve(res.data) } else if (res.statusCode 401) { uni.reLaunch({ url: /pages/login/index }) reject(new Error(未登录)) } else { uni.showToast({ title: res.data.message || 请求失败, icon: none }) reject(new Error(res.data.message || 请求失败)) } }, fail: (err) { if (err.errMsg err.errMsg.includes(timeout)) { uni.showToast({ title: 请求超时请重试, icon: none }) } else { uni.showToast({ title: 网络异常请检查网络, icon: none }) } reject(err) } }) }) }这段代码有两个设计点值得说。第一是缓存策略我给 GET 请求加了一个 5 分钟的默认缓存同样的查询在短时间内不会重复打服务器。这对地图周边检索这类高频接口特别友好用户来回滑动地图每次拖动都触发一次请求没有缓存的话光地图页就能把云函数的高频请求配额打爆。第二是 401 的设计。token 失效是不可避免的关键是拿到 401 之后不能无脑跳登录页否则用户正在浏览的上下文就丢了。我当时的处理是在登录页保留一个 redirect 参数登录成功后自动跳回原页面。这个小细节直接影响用户体验很多人会忽略。超时时间设置 10 秒也是经验之谈。短了在弱网环境老是断长了用户干等着急。10 秒对大部分 JSON 接口都够用如果业务里有大文件上传单独给那类请求覆盖timeout参数即可。3.2 动态标题与顶部导航栏适配微信小程序的导航栏是很多新手的老大难问题。不同的机型、不同的导航栏样式状态栏高度和胶囊按钮位置都不一致如果写死一个padding-topiPhone 14 Pro 上可能正常小米上就顶到刘海屏里了。我封了一个获取导航栏高度的公共方法// utils/system.js export function getNavHeight() { const systemInfo uni.getSystemInfoSync() const menuButton uni.getMenuButtonBoundingClientRect() const navBarHeight (menuButton.top - systemInfo.statusBarHeight) * 2 menuButton.height return { statusBarHeight: systemInfo.statusBarHeight, // 状态栏高度 navBarHeight: navBarHeight, // 导航栏总高度 menuButtonHeight: menuButton.height, menuButtonTop: menuButton.top } }很多人不知道uni.getMenuButtonBoundingClientRect这个接口它返回右上角胶囊按钮的位置信息。这个数据是全机型适配的关键通过它反推导航栏高度基本不会出错。我把这个方法放在onLaunch阶段调用塞到全局数据里后面所有自定义导航栏的页面都从全局读取。动态标题的需求来自榜单页。比如人气榜的标题我想让用户下拉刷新时显示“榜单更新中”刷新完再变回“人气榜 TOP50”。这个场景用uni.setNavigationBarTitle就能实现uni.setNavigationBarTitle({ title: 榜单更新中... })但要注意这个接口只能改导航栏的文字不能改导航栏的背景色。想换背景色得用navigationBarBackgroundColor这个pages.json配置它是全局生效的不能动态改。如果你希望每个页面有独立导航栏背景建议直接用自定义导航栏模式在pages.json里设置navigationStyle: custom完全自己画自由度最大。这里提一个网上讨论比较多的问题pages.json里点击右上角胶囊按钮的“...”菜单能不能自定义里面的选项。答案是不能完全自定义微信只允许你配置转发、收藏等内置功能不能往菜单里塞自己的按钮。我之前想加个“投诉建议”入口进去最后老老实实改成在页面内布局不要再折腾这个了。3.3 支付与订单流程uniapp 跨端支付的差异美食点评类小程序一般会有会员、优惠券这类虚拟商品或者和线下商家联动的到店订单。这就涉及支付模块。我在 uniapp 里同时做了微信小程序和 App 端的支付两种端在流程上有一些差异一开始我以为代码能完全复用实际写的时候才发现还是有不少细节要处理。先说微信小程序端的支付。流程是前端请求自己的后端创建一个支付订单后端拿到订单号、金额、商品描述这些信息后调用微信支付接口生成payment参数返回给前端。前端再用uni.requestPayment把这个参数传给微信客户端弹起收银台// 小程序端拉起支付 uni.requestPayment({ provider: wxpay, timeStamp: payment.timeStamp, nonceStr: payment.nonceStr, package: payment.package, signType: MD5, paySign: payment.paySign, success: (res) { // 支付成功以服务端回调为准不要只信任前端返回 }, fail: (err) { // 用户取消或支付失败 } })如果是 App 端uni.requestPayment的参数会有一点不同它不认timeStamp、nonceStr这一套微信小程序的参数格式而是用provider、orderInfo这样的结构。所以如果你做的是多端支付相关的代码还是要分开写不能一个函数通吃。前端统一调用uni.requestPayment但具体传给它的参数对象要在小程序和 App 各自构造。再讲一个很多人容易入坑的点支付成功回调。前端收到success事件并不代表钱真的到账了。微信有专门的支付回调接口是打到你服务端的不是打给小程序前端的。正确的做法是前端支付成功后立即向后端请求一次“查询订单状态”以后端订单状态变成“已支付”为准来更新 UI。这样能避免前端被伪造回调、或者小程序杀掉重开导致订单状态不一致的问题。虚拟支付这块要特别提醒微信对“虚拟支付”管控很严。美食点评类的会员充值、付费问答这类功能如果被判定为虚拟支付违规轻则功能被下架重则整个小程序被限制支付能力。我一开始做了个“余额充值”的功能被驳回后改成“会员订阅”最后还是不行最后只能绕回到线下场景。所以做这类设计前先仔细读一遍微信支付的类目要求别等提交审核了才发现路走不通。4. 从开发到上架的坑我替你踩过了4.1 开发者工具版本过期与真机预览失败开发到一半经常碰到开发者工具提示“开发版小程序已过期请在开发者工具重新扫码”。这个提示其实是故意设计的机制因为小程序开发版的有效期只有 24 小时过期后需要重新在开发者工具里点击“预览”再用手机微信扫码才能继续使用。这个机制本身没什么问题烦人的是你正在真机调试某个交互流程中间扫了个码调试上下文就断了。我的习惯是每次真机调试前先确认开发者工具是登录状态并且项目没有过期。另外一个小贴士在开发者工具的“详情-本地设置”里把“热重载”打开修改代码后模拟器和真机都能快速更新不用频繁扫码。真机预览失败的另一个常见原因是域名白名单问题。小程序在真机上请求的 URL 必须在小程序后台配置过合法域名否则请求会被拦截。开发者工具里可以勾选“不校验合法域名”但真机没有这个选项必须到“小程序后台-开发-开发设置-服务器域名”里配置。如果你用的是云开发网关域名是微信自己的不需要额外配置。我当时从云开发切换到自定义 API 域名时第一版测试直接全部请求失败就是这个白名单导致的。4.2 网络请求排查本地调试时如何看请求数据小程序开发最常见的问题就是“为什么我这边数据不对”这时候你需要一个可靠的请求排查手段。开发者工具自带的 Network 面板是最直接的工具它在调试器里能看到每个请求的 URL、请求头、响应体包括uni.request发出去的所有网络调用。真机调试时开发者工具的 Network 面板就不太好用了我推荐用抓包工具在 PC 端统一排查。在 PC 上开一个抓包代理手机和 PC 连同一个局域网把手机 HTTP 代理指向 PC 的监听端口然后手机上打开小程序的每个页面所有请求和响应都能实时看到。这个方法对排查接口数据格式、定位问题特别高效。不过在真机上抓包微信会对 HTTPS 证书做校验抓包工具需要在手机上安装根证书才能解密流量。这一步是正规调试流程你拿自己的小程序、自己的测试环境做调试完全没问题。顺便提醒一句抓包能力再强也别往反编译、破解别人小程序的方向上靠合规风险很高而且对技术成长没什么帮助。4.3 年审、类目资质与内容合规小程序的审核和年审是大部分人容易忽视、但真的会影响产品存亡的环节。微信小程序的个人主体和企业主体年审时间不一样一般要求每年做一次年审没有按时年审的小程序会被暂停服务。不少开发者做完第一版上线后就把这件事忘了结果一年后突然发现线上小程序打不开了才想起来年审没做。美食点评类小程序在申请类目时要注意选择合适的类目。我当时选的“餐饮美食-美食点评”审核要求提供相关的资质证明比如营业执照、食品安全相关的许可。如果你只选“工具-信息查询”审核可能更松但功能展示上会有约束很多餐饮相关的交互会被判为超范围。我的建议是前期想清楚你要做的是内容平台还是营销工具两者对应的类目和资质完全不同。内容合规上也得留个心。用户点评里可能出现违规内容比如广告导流、不文明用语、甚至不实的消费信息。小程序后台有内容安全检测接口但我建议在前端发布点评时就做一次检测避免违规内容进了数据库再删。我在发布点评的页面接入了文本检测在提交评论前异步调用一旦检测结果有风险就直接拦截用户在界面上会看到一个友好的提示而不是“提交失败”。食品安全类的敏感词比如“过期”“变质”这类单独加了人工运营的后台标记因为这类词有时候是真实吐槽直接一刀切删了会伤用户感情。另外很多点评类小程序还涉及用户地理位置数据的处理隐私协议里必须写清楚“会收集位置信息用于推荐附近店铺”并且在真机首次调用定位功能时通过微信的隐私弹窗向用户说明。这个如果漏掉新版微信会对“未声明收集位置信息却调用定位”的小程序做接口拦截用户根本拿不到定位结果。4.4 性能优化首屏加载与图片体积控制美食点评类应用图片是命根子也是性能杀手。我第一次上线时没做图片压缩结果首屏加载一个大列表几十张高清图直接在微信小程序里跑卡得用户滑动都掉帧。后来我总结了几条规则第一所有上传的店铺图片和点评图片统一压缩到宽度 1080px 以内因为手机屏幕物理像素通常不会超过这个宽度再大用户也看不清白白浪费带宽和内存第二启用 CDN 加速图片访问不要在云存储的默认域名上裸奔第三列表缩略图用低清版本点击放大时才加载原图这个和视频的“封面播放”是一个逻辑。首屏加载还有一招就是在onLoad阶段先渲染本地缓存的店铺数据再通过请求拉更新。用户打开小程序看到的是 0 秒出内容等请求完成后自动刷新。这个做法虽然牺牲了一点数据的实时性但对留存率的提升非常明显。毕竟现代社会的人耐心就那么几秒你让他转圈圈等 3 秒他直接划走了。5. 运营期避坑数据埋点与版本迭代说完了开发和上架接下来说运营。小程序上线只是第一步后面日常维护的工作量比你想象的要多得多。我这版加了数据埋点统计用户访问首页、点击搜索、查看店铺、提交点评这几个关键行为的转化率。这个数据是后续迭代的方向盘没有数据做参考你永远不知道哪里好哪里坏。埋点的实现不用复杂的 SDK直接用uni.report这个接口就能上报事件。我在utils/track.js里封装了统一的埋点方法// utils/track.js export function track(event, data {}) { try { uni.report(food_app_event, { event, time: Date.now(), ...data }) } catch (e) { // 埋点失败不影响主流程静默处理 } }埋点数据怎么用我以“搜索结果点击率”为例用户在搜索框输入“火锅”点击了第几个结果这个数据能反映搜索相关性强不强。如果大部分人点了前三位但跳出率高可能是首屏展示的信息不够比如缺人均价、缺评分运营上要做的就是把首屏卡片的字段补全。如果前三位几乎没人点可能是搜索召回有问题关键词没有匹配到真正的热门餐厅。另外一个容易被忽略的运营问题是版本迭代的节奏。小程序发版快但不能每次改完就提交审核这样既消耗审核额度也容易让用户觉得产品不稳定。我现在的节奏是功能更新攒一次比如一周或者十天一发版集中在周四提交审核保证周五能上因为周末是吃喝玩乐类小程序的流量高峰。你要是周一提审遇到审核排队可能周四才过刚好错过大流量时段。这个节奏看起来很小但对业务数据的影响其实非常大。6. 最后再分享几个我留到最后才说的经验文章写到这里已经很长了但有几个经验我是刻意放在最后说的因为它们不属于某个模块而是贯穿整个项目的思维层面的东西。第一点是关于技术栈的执念。如果你不是要给代码库做长期维护就不要在小程序技术选型上花太多时间纠结。uniapp、Taro、原生小程序它们之间的差距没有社区吵得那么大。真正拉开产品差距的是你对业务场景的理解比如美食点评场景里为什么首页信息流要按“距离评分”排序因为用户不是来看文章的他们是来找地方吃饭的这个洞察比任何框架都值钱。第二点是对数据资产的珍惜。点评内容、用户行为数据、店铺标签体系这些东西会随着时间积累成为你的核心竞争力。我见过一些小团队三个月做出一款小程序但内部数据模型一塌糊涂没有标签字段、没有浏览记录、没有行为埋点。等他们想加推荐算法时发现历史数据全是脏的根本没法用。所以每张表的设计能留的字段尽量留哪怕暂时用不到总比后期翻表重来容易。第三点是学会冷启动。很多个人开发者或者小团队最大的误判是高估了小程序的自然流量。微信确实会给优质小程序一些曝光但美食点评这种垂直领域的小程序初期用户只能靠你自己去拉。我当时第一批种子用户是跑了十几家线下餐饮店一家一家谈合作换来的。这个过程虽然辛苦但用户反馈和真实数据远胜任何付费推广。没有这第一步做冷启动后面优化的驾驶感会很差。这三条都是没法写进代码里的经验但值得反复咀嚼。你可以把产品功能做得很酷但决定它能走多远的往往是这些看起来“软”的东西。
返回列表