简介:这是一份面向微信小程序初学者的菜谱大全项目源码,以美食菜谱为业务场景,完整演示了从页面搭建到交互逻辑的小程序开发流程。资源共26个文件,压缩包仅19KB,主要包含6个js逻辑文件、5个wxss样式文件、4个wxml页面结构文件、3个png图片资源以及json、xml等配置信息,覆盖菜单、搜索、详情等页面模块;整体目录结构清晰,从app.json全局配置到列表、搜索、详情等子页面分模块摆放,并附有README与修改日记,便于对照学习与二次开发。目前已有2106人学习下载,适合希望借助实际项目快速上手小程序开发的读者;通过阅读这份资源,可以掌握路由管理、数据请求、本地缓存、列表展示与页面交互等核心知识点,是一份轻量且实用的入门级参考。
1. 项目定位与整体设计:为什么菜谱类小程序值得做
1.1 菜谱小程序的需求画像与核心功能
做微信小程序开发这几年,我接过的项目里最常被问到的就是“想做个菜谱大全类的应用,难不难”。说实话,内容型小程序里,菜谱、资讯、知识库这几种是典型到不能再典型的形态,需求足够垂直、调用微信生态能力足够多,用来练手或者做独立项目都非常合适。
菜谱小程序的核心用户场景很清晰:用户在做饭时打开小程序,快速查询“今天吃什么”,搜索某道菜的做法,查看食材用量和步骤,可能还要收藏菜谱、记录自己的做法,甚至生成购物清单。这些都是高频率、短时长的操作,恰好匹配小程序“即用即走”的产品属性。和纯H5网页版比,小程序能享受微信的账号体系、模板消息、微信支付、分享卡片等原生能力;和原生App比,又省去了下载安装流程,很适合菜谱这种“想起来了才看一眼”的工具型应用。
从功能拆解来看,一个完整的菜谱小程序应该包含这几个模块:分类导航(家常菜、川菜、烘焙等)、关键词搜索、菜谱详情页(食材、步骤、视频)、收藏与历史记录、个人中心。如果再往商业化方向走,还可能加上会员付费、积分签到、专栏课程、直播预告等功能。本文就围绕我实际开发菜谱类小程序过程中遇到的高频问题展开,里面大部分坑都是微信小程序开发者会踩到的,属于一眼看过去就能少走弯路的经验。
1.2 技术选型:原生小程序还是 uniapp
做菜谱小程序之前,首先要决定用原生微信小程序还是跨端框架。这个选择直接决定了后面踩坑的方式。我把两种方案的差异整理成了表格,方便你对照自己的情况:
| 对比维度 | 原生微信小程序 | uniapp |
|---|---|---|
| 上手成本 | 需要学习WXML/WXSS,入门快 | 会Vue就能写,跨端能力强 |
| 性能体验 | 最好,渲染控制最细 | 多一层编译,复杂页面有损耗 |
| 微信API覆盖度 | 最新API都能第一时间使用 | 部分能力需要封装或等社区更新 |
| 调试体验 | 微信开发者工具原生支持 | 依赖HBuilderX联动,偶尔抽风 |
| 多端发布 | 仅微信 | 可打包App、H5、各端小程序 |
我自己的习惯是:如果是纯微信端且重视体验细节,选原生;如果后续要考虑App端,选 uni-app。本文里讲到的导航栏高度、软键盘遮挡、视频全屏错位这类问题,在原生和 uniapp 里都会遇到,处理思路是一致的。后面除非特别说明“uniapp中”,大部分内容两种技术栈通用。
2. 从页面搭建到核心功能实现
2.1 自定义导航栏:顶部高度与胶囊对齐
菜谱类小程序对页面要求干净清爽,默认导航栏的背景色和字体颜色控制范围有限,很多团队会选择自定义导航栏。但自定义导航栏的第一道关卡就是“顶部安全距离”的适配,这也是“微信小程序自定义标题上边距怎么弄”这类问题被反复搜索的原因。
微信小程序的自定义导航栏核心是两个数据:状态栏高度(statusBarHeight)和胶囊按钮的位置。状态栏高度就是手机顶部显示电量、信号的那一条,通过wx.getWindowInfo()或者wx.getSystemInfoSync()获取。胶囊按钮的位置需要用wx.getMenuButtonBoundingClientRect()获取,这个方法返回胶囊按钮的上下左右坐标和宽高。
导航栏的真实高度该怎么算?我直接给出一个稳定可靠的公式:
const windowInfo = wx.getWindowInfo(); const menuButton = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = windowInfo.statusBarHeight; // 状态栏高度 const navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height; // 导航栏高度 const totalHeight = statusBarHeight + navBarHeight; // 导航栏总高度为什么是(menuButton.top - statusBarHeight) * 2 + menuButton.height?因为胶囊按钮在导航栏垂直方向上是居中的,胶囊上边到状态栏底部的距离,加上胶囊下边到导航栏底部的距离,两者是对称的。所以先用胶囊顶部位置减去状态栏高度得到“上方留白”,乘以2后再加上胶囊本身高度,就得到了导航栏的总高度。
这里有个很重要的细节:menuButton.top - statusBarHeight这个值在安卓和iOS上并不相同,部分安卓机型的差异甚至能达到几像素到十几像素,所以不要在代码里写死,必须每次动态计算。还有,iPhone 灵动岛机型的状态栏高度大约在59px左右,比传统机型高,如果不适配,导航栏内容会顶到挖孔区域。正确获取之后,自定义标题栏的容器高度直接设为totalHeight,内部内容垂直居中即可。
我在实际项目中还发现,部分安卓低端机上wx.getMenuButtonBoundingClientRect()偶尔会返回空对象,尤其是页面刚启动时调用。稳妥做法是加一个 fallback:如果拿不到胶囊按钮位置,就按黄金比例分配,navBarHeight = statusBarHeight * 1.6,虽然不完全精确,但至少不会导致导航栏彻底错位。
2.2 搜索页与软键盘遮挡问题
菜谱小程序里搜索是核心入口,用户要快速输入菜名,但“uniapp 微信小程序 手机软键盘会遮挡住查询内容”这个问题在原生小程序里也频繁出现。原因在于:软键盘弹出时会把输入框顶起来,但如果页面里搜索结果的容器是固定高度,或者页面没有正确滚动,键盘就会直接盖住当前正在查看的内容。
解决这个问题有几个层次。第一层:给输入框设置cursor-spacing属性,这个属性表示输入框光标与键盘之间的距离,单位是px,建议设置成cursor-spacing="20",让输入框和搜索结果列表底部与软键盘保持至少20px的间距。
第二层:给页面根节点设置adjust-position。在原生小程序里,页面配置或输入框所在的input组件默认会在键盘弹出时自动上推页面,但如果adjust-position="{{false}}",就需要自己监听键盘高度并做页面滚动。建议保持默认的true,让系统自动处理基础场景。
第三层是uniapp开发中比较头疼的情况:使用自定义导航栏时,页面上推的高度计算不对,结果被键盘顶起来后导航栏跟着乱跑。我的处理方式是监听onKeyboardHeightChange事件,拿到键盘高度后手动设置底部容器的 padding-bottom。
// 小程序页面生命周期中监听 onKeyboardHeightChange(e) { const { height } = e; this.setData({ // 将页面底部安全区域撑高,把内容顶出键盘区域 keyboardHeight: height }); }<!-- 在搜索结果容器上动态绑定padding-bottom --> <view class="search-list" style="padding-bottom: {{keyboardHeight}}px;"> <!-- 搜索列表内容 --> </view>还有一点容易被忽略:在 iOS 上,软键盘的“搜索”按钮触发条件是小程序 input 组件设置confirm-type="search",同时配合bindconfirm事件处理搜索逻辑。很多新手只监听bindinput,导致用户按下键盘上的搜索键没反应,体验差一大截。
2.3 菜谱列表、详情与图片保存
菜谱列表页的渲染优化是内容型小程序的重要环节。数据量一大,一次性setData上万条数据,页面铁定卡顿,甚至白屏。我的做法是前端分页加载,每页20条,配合onReachBottom触底加载下一页。同时,图片列表用懒加载属性lazy-load,服务端返回的图片地址加裁剪参数,缩略图宽度控制在 300px~400px。这些细节对真实小程序体验影响极其明显,做好之后滚动流畅度完全是两个级别。
给图片加效果这种事,听起来很简单,但“uniapp微信小程序保存图片:saveImageToPhotosAlbum:fail”这个报错,我见过太多团队卡在这里。原因无非三种:一是没有获取相册权限;二是图片路径不是本地路径;三是用户之前拒绝过权限,之后再次调用直接失败。
保存图片的标准流程应该是这样的:
- 调用
wx.getSetting检查是否已获得scope.writePhotosAlbum权限; - 如果没有,调用
wx.authorize请求授权; - 如果用户拒绝过授权,
wx.authorize会直接进入 fail 回调,此时必须用wx.openSetting引导用户去设置页手动开启; - 图片如果是网络地址,不能直接用来保存,需要先用
wx.downloadFile下载到本地临时路径,再调用保存接口。
function saveImageToAlbum(imageUrl) { wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { // 已授权,直接下载并保存 downloadAndSave(imageUrl); } else if (res.authSetting['scope.writePhotosAlbum'] === false) { // 之前拒绝过,引导打开设置页 wx.openSetting({ success(settingRes) { if (settingRes.authSetting['scope.writePhotosAlbum']) { downloadAndSave(imageUrl); } } }); } else { // 首次请求授权 wx.authorize({ scope: 'scope.writePhotosAlbum', success() { downloadAndSave(imageUrl); }, fail() { wx.showToast({ title: '需要相册权限才能保存', icon: 'none' }); } }); } } }); }另外,saveImageToPhotosAlbum在 iOS 和安卓上的表现也有差异。安卓上部分机型如果保存的是 base64 临时文件路径,可能直接失败;iOS 对图片格式要求更严格,网络图片下载失败时返回路径为空,保存自然失败。所以每次下载完成后都要校验res.statusCode === 200和res.tempFilePath存在,不能想当然直接保存。
3. 支付、网络与多媒体功能的实战细节
3.1 微信支付 v3 对接与“无可用的平台证书”问题
菜谱小程序如果要做会员或者付费专栏,绕不开微信支付。这几年新商户基本都要求对接 APIv3,而开发者在对接时最常遇到的报错就是“微信支付v3对接 无可用的平台证书,请在商户平台-api安全申请使用微信支付公钥”。
先说清楚 V3 支付的整体结构:商户号、AppID、APIv3密钥、商户私钥、商户证书序列号、平台证书/微信支付公钥。相比 V2 用 API 密钥做 HMAC-SHA256 签名,V3 使用 RSA-SHA256 签名,安全性更高,但也更繁琐。
“无可用的平台证书”这个错误的根因是:微信支付 V3 的接口响应内容使用平台证书的公钥加密,商户需要用平台证书解密。如果你的服务器上根本没有存平台证书文件,SDK 在需要解密时就找不到可用证书,于是抛错。
解决方式有两种。第一种是手动下载平台证书:登录微信支付商户平台,在“API安全”菜单里申请获取平台证书,然后下载到服务器,配置到 SDK 的证书路径中。第二种是使用官方 SDK 的自动更新功能,比如 Java 的wechatpay-java和 PHP 的wechatpay-php都支持通过certificates下载器定期获取并更新平台证书。自动更新的好处是一劳永逸,平台证书每 5 年更换一次,手动维护容易忘记。
需要注意:2023年后微信支付逐步推进“微信支付公钥”替代“平台证书”的策略。如果你是新的商户,在商户平台看到的可能不是“下载平台证书”,而是“申请微信支付公钥”。对接时以商户平台实际显示的为准。如果你在网上搜到的教程还在让你去下载平台证书,但你的商户平台界面并没有这个入口,大概率是微信支付已经给你切换到了公钥模式,这时候就不用再纠结“没证书”的问题,只需要下载公钥并配置到SDK里。
另外,我还遇到过一个非常离谱的情况:小程序本身没有违规,但支付功能突然提示“由于小程序违规,支付功能暂时无法使用”。这个问题通常是平台侧的处罚限制,不会因为代码修复而自动恢复。处理路径只有一条:登录微信公众平台,查看站内信和违规记录,确认具体违规原因,按指引发起申诉。如果是误判,申诉后一般1-3个工作日会恢复。如果是确实违规,需要整改后提交申诉或等待处罚期结束。所以,菜谱类小程序如果涉及用户生成的菜品评论、图片上传,一定记得接入微信的security.msgSecCheck和security.imgSecCheck内容安全接口,否则被用户投诉或者被系统检测出违规内容,轻则删帖,重则封支付权限甚至下架。
3.2 全局网络异常提示的统一实现
移动端网络波动是常态,“当网络不可用或者网络不好的时候,如何全局统一显示网络不可用”这个问题很多开发者处理得很零散,每个页面单独判断,代码重复量大,还容易出现漏网之鱼。我的方案是在请求层和全局层双管齐下。
请求层:封装统一的request方法,在回调里统一处理错误码和网络错误。当wx.request的 fail 回调被触发,或者返回的 statusCode 不属于 2xx,就通过一个全局状态管理器记录当前“网络异常”状态。所有页面只需要监听这一个状态,不需要各自写wx.getNetworkType。
全局层:在App的onLaunch里注册wx.onNetworkStatusChange监听,网络断开或者从WiFi切到移动网络时主动更新状态。然后在小程序入口页面上挂一个全局的通用的“网络不可用”提示组件,监听全局状态,异常时显示遮罩条或全屏提示。
// app.js App({ onLaunch() { wx.onNetworkStatusChange((res) => { // 通知所有页面 const pages = getCurrentPages(); pages.forEach((page) => { if (page.onNetworkStatusChange && typeof page.onNetworkStatusChange === 'function') { page.onNetworkStatusChange(res); } }); }); } });// 页面中使用 Page({ data: { networkNotAvailable: false }, onNetworkStatusChange(res) { this.setData({ networkNotAvailable: !res.isConnected }); } });这样做有个好处:网络恢复时,res.isConnected会变回true,页面可以自动隐藏提示并重新拉数据,不需要用户手动刷新。实际开发中,我还加了防抖逻辑,避免网络在3秒内反复切换导致提示闪烁。具体做法是记录上次状态变化时间,两次变化间隔小于3秒就忽略当前变化。
3.3 video 组件与 swiper 嵌套的坑
菜谱详情页如果带视频教程,视频播放是最容易出问题的地方。最常见的是“微信小程序 iOS中swiper组件嵌套video组件导致全屏错位解决方案”和“微信小程序 video不能播放”。
先解释 iOS 上 swiper 嵌套 video 的错位问题:video 组件本质上是原生组件,就算开启了same-layer-rendering(同层渲染),在 iOS 上全屏切换时,video 的层级和坐标计算依然容易和 swiper 的内部动画冲突,导致全屏后画面偏移或者黑屏。
我的解决方案是:全屏时不依赖 swiper 的自动滑动,而是在bindfullscreenchange事件里手动处理。
onFullscreenChange(e) { const isFullscreen = e.detail.fullScreen; if (isFullscreen) { // 全屏时暂停swiper自动轮播,防止内部动画干扰 this.setData({ autoplaySwiper: false }); } else { this.setData({ autoplaySwiper: true }); } }同时,给 swiper 里的 video 设置object-fit="contain",并确保页面级配置里开启"sameLayerRendering": true。如果你只在 swiper 的第一屏放视频,后面都是图片,建议不要用 swiper 包裹视频,改成横向 scroll-view,这样可彻底避开嵌套问题。
至于“video不能播放”,我整理过一份排查清单,按出现频率排序:
- 合法域名没有配置:视频源如果是网络地址,域名必须在小程序后台“开发管理-服务器域名-业务域名”里配置并校验通过。开发工具里勾选“不校验合法域名”只能缓解本地调试,真机上依然会被拦。
- 视频格式不支持:小程序 video 组件对 HLS(m3u8)和 MP4 支持最好,部分安卓机型对 FLV、MKV 格式支持差,建议服务端转码为 HLS 或 MP4。
- 没有绑定
src到合法路径:视频源地址必须是https://开头的网络地址或本地临时路径,file://在真机上基本不可用。 - autoplay 被限制:小程序对视频自动播放有策略限制,iOS 上静音自动播放可以,带声音自动播放会被拦截,建议不要依赖自动播放,让用户点击后播放。
4. 调试、抓包与发布流程
4.1 使用 burp suite 抓取微信小程序请求
开发和排查线上问题时,抓包是调试网络请求的必备手段。很多做安全的工程师习惯用 burp suite,我发现不少小程序开发者也开始用 burp 抓小程序数据包来分析接口字段。
Burp Suite 抓取 PC 端微信小程序的思路是这样的:把手机或PC上的微信流量通过 HTTP 代理转发到 burp,burp 作为中间人捕获 HTTPS 请求。但微信小程序默认不信任用户的根证书,所以我们手动安装 burp 的 CA 证书到系统后,还需要在微信开发者工具中关闭 HTTPS 证书校验,或者将证书导入到手机/PC的系统证书库。
我常用的操作流程:
- 启动 burp,在 Proxy Options 里设置监听地址为本机IP和端口(如 127.0.0.1:8080);
- 在微信开发者工具中,打开“设置-代理设置”,选择手动代理并填入 burp 的地址和端口;
- 如果抓真机上的小程序,手机和电脑连同一个WiFi,手机WiFi设置里配置HTTP代理为电脑IP+burp端口;
- 用浏览器访问
http://burp下载 CA 证书,在手机设置中安装证书并信任; - 回到小程序开发工具,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。
这些流程做完,burp 里就可以看到小程序的请求流量,包括接口 URL、请求参数、响应内容。有一点要特别提醒:抓包只适用于排查自己的项目,不要去抓别人的生产环境小程序,一方面不道德,另一方面可能涉及隐私风险。
4.2 HBuilderX开发小程序的各种报错与真机调试
如果你用 uniapp 开发,HBuilderX 直接 “运行到微信开发者工具” 时,最经典的问题就是 “hbuilder运行微信小程序提示不是开发者”。这个报错的原因基本只有一个:微信开发者工具安全设置里的“服务端口”没有开启,或者当前登录微信号不是该项目 AppID 的开发者。
解决步骤非常固定:
- 打开微信开发者工具,点击右上角“设置-安全设置”;
- 打开“服务端口”开关;
- 确认 HBuilderX 和微信开发者工具登录的微信是同一个账号;
- 在微信公众平台后台,把当前微信号添加为项目开发者。
也有一种情况是 HBuilderX 内置的微信开发者工具路径配置错误,在 HBuilderX 的“运行-运行到小程序模拟器-运行设置”中手动指定微信开发者工具的安装路径,尤其是 mac 上经常需要定位到/Applications/wechatwebdevtools.app/Contents/MacOS/cli。
真机调试时还要注意:如果页面请求的接口域名没配到小程序后台,真机上直接请求失败。本地开发时建议使用微信开发者工具里“本地调试”自带的代理功能,或把接口环境切换成测试域名并配好合法域名,“不校验合法域名”只对开发工具有效,真机无效。
4.3 小程序发布上架与审核避坑清单
菜谱小程序从开发到上线,发布流程本身并不复杂:在微信开发者工具点击“上传”,填写版本号和备注,然后在公众平台“版本管理”里提交审核,审核通过后点击“发布”。真正容易出问题的是类目选择和审核拒绝。
菜谱类小程序建议选择“餐饮-菜谱”或“美食-菜谱”类目,如果你只是做个内容展示工具,不需要特殊资质。但如果涉及用户上传菜品、UGC评论、付费会员,就可能需要《增值电信业务经营许可证》(ICP)备案或对应的文化经营资质,不同的主体类型要求不一样,最好提前在公众平台的“服务类目”里查清楚再开发。
审核被拒的高频原因,我整理了这份检查项:
| 审核拒绝原因 | 解决办法 |
|---|---|
| 缺少用户隐私保护指引 | 在公众平台设置“用户隐私保护指引”,详细声明收集哪些信息、用途是什么 |
| 登录功能未符合规范 | 不能强制用户授权手机号,必须允许用户以游客身份浏览内容 |
| 诱导分享或诱导关注 | 去掉“分享得积分”“关注解锁”等诱导式按钮 |
| 内容存在风险 | 接入内容安全接口,过滤用户发布的评论和图片 |
| 类目选择错误 | 重新选择与实际功能最匹配的服务类目 |
5. 常见问题排查速查表
5.1 从热搜词里收集的高频问题清单
开发这几年,我养成了一个习惯:每隔一段时间就去翻一遍微信小程序相关的热搜词,看看大家最近都在踩什么坑。整理出来的这些问题,大部分不是高深的技术难题,而是细节上的坑。下面这份速查表送给你们,拿去对照排查即可:
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 安卓手机蓝牙搜索不到设备 | 定位权限未开启;蓝牙扫描需要同时开启位置服务 | 在app.json中声明requiredPrivateInfos,调用蓝牙前检查定位权限 |
| 保存图片到相册失败 | 授权被拒、图片路径不合法 | 参考本文 2.3 节,先下载再保存,被拒绝时用openSetting引导 |
| 自定义导航栏返回箭头消失 | 页面配置navigationStyle: custom时自绘返回按钮被遮挡 | 在页面中添加自定义返回按钮,并通过getMenuButtonBoundingClientRect计算位置 |
| video 全屏后错位 | swiper 与 video 原生组件层级冲突 | 绑定fullscreenchange,全屏时暂停 swiper 自动播放,或改用 scroll-view |
| 单选框样式不生效 | 原生 radio 默认样式不易覆盖 | 使用label包裹自定义样式,或用 van-radio 等第三方组件 |
| 课程表小程序提醒不生效 | 订阅消息需用户主动订阅且一次性 | 引导用户多次订阅,并记录下一次提醒时间,在服务端使用定时任务下发 |
| 身份证引导拍摄不准 | 摄像头/相册权限未处理,或相机预览比例不对 | 检查授权状态,使用camera组件展示身份证取景框,并把拍摄结果传给 OCR 接口识别 |
| 内嵌H5后工具栏左侧返回箭头消失 | H5页面内没有调用wx.miniProgram.navigateBack | 在H5中引入微信JS-SDK,返回按钮绑定wx.miniProgram.navigateBack() |
5.2 头像昵称获取逻辑调整后的处理建议
微信小程序最近几年调整了用户头像昵称的获取规则,以前一句wx.getUserProfile就能拿到头像和昵称,现在接口已经下线,用户授权后会拿到“微信用户”的通用头像和“微信用户”的昵称占位。菜谱类小程序如果需要用户自定义昵称,现在的主流做法是使用微信官方提供的“头像昵称填写能力”:用户点击头像组件时,可以调用wx.chooseMedia让用户选择本地图片作为头像;昵称则通过input组件的type="nickname"让用户直接填写。这既能保证合规,又不会让用户因为隐私顾虑放弃登录。
这个改动对开发者影响很大,如果你的项目还是按旧逻辑去获取真名和头像,用户在真机上体验到的就是“微信用户”这个占位名,看起来很怪。我建议菜谱小程序里,将头像和昵称改为可编辑的个人资料模块,用户自己选择是否补充完整信息,不要强制。
5.3 最后分享几个冷门但实用的开发技巧
第一个:小程序顶部导航栏渐变色。原生navigationBarBackgroundColor只支持纯色,想要渐变只能自定义导航栏,然后给导航栏容器加background-image: linear-gradient,同时需要注意渐变区域要覆盖到状态栏。
第二个:小程序的分享卡片图片需要用onShareAppMessage里的imageUrl字段,图片必须是https://且建议 5:4 比例,菜谱类应用建议用成品菜的高清横图。还有就是分享出去后,用户点击卡片进入页面时要带一个来源标记参数,方便统计分享带来的流量。
第三个:开发时一定要开vconsole看一下真机日志,很多问题在开发者工具里不复现,但真机上必现,日志是唯一定位方式。
做菜谱小程序这件事,技术难度不高,真正的门槛在细节适配和合规处理上。上面这些坑,我自己都踩过,反复查文档、看社区、尝试各种方案才总结出来,希望能帮你少走一些弯路。如果你正在开发类似的内容型小程序,遇到这里没写到的问题,不妨按“页面表现 → 接口数据 → 权限配置 → 平台限制”的顺序排查,大部分问题都能在这个链路里找到答案。
本文还有配套的精品资源,点击获取