简介这是一份面向微信小程序初学者与实战开发者的菜谱类应用完整源码聚焦美食内容展示与用户交互功能实现适用于课程设计、毕业项目或快速入门练习。资源包含26个文件涵盖6个JS逻辑层代码处理页面交互与数据请求、5个WXSS样式文件构建响应式UI、4个WXML模板定义菜品列表、详情、搜索等页面结构辅以PNG图片资源、JSON配置及README说明文档整体仅19KB轻量易读。已有2111人学习下载代码结构清晰首页轮播分类导航搜索收藏功能完整内置wx.request网络请求、本地缓存、页面路由跳转及事件绑定等核心API实践可直接运行调试并作为微信小程序开发的典型范例参考。1. 微信小程序菜谱大全不是模板套壳而是能真上线、真查菜、真适配新版基础库的完整工程包你是不是也试过在 GitHub 或某资源站搜“微信小程序 菜谱”结果下回来的是一堆空项目结构、只有app.js和三张占位图的“教学Demo”或者更糟——代码里硬编码了 200 条菜名、用wx:for直接遍历 JSON 数组连搜索框都只是个input标签加个console.log这个「微信小程序-菜谱大全」不是那种。它是一个已通过微信开发者工具 v1.06.2312010 基础库 3.4.5 实测可编译、可预览、可真机扫码运行的完整小程序工程包包含真实可用的本地数据分层分类/标签/详情/收藏状态、带防抖的模糊搜索、支持图片懒加载与本地缓存的菜品详情页、以及关键的——所有 WXML 结构和 WXSS 样式均按微信官方《小程序无障碍开发规范》做了语义化标注与焦点管理。适合正在从零搭建第一个生产级小程序的前端新人也适合需要快速验证 UI 交互逻辑、跳过环境配置直接调试业务流的熟手。它不教你怎么注册 AppID但每一步npm run dev后能看到什么、wx.getStorage返回值长什么样、点击收藏按钮时setData的 diff 是哪几行都清清楚楚。2. 工程结构与核心模块拆解从project.config.json到pages/recipe/detail2.1 为什么选这套目录结构避开新手最常踩的“单页巨无霸”陷阱很多初学者一上来就建一个pages/index/index把所有功能塞进一个 Page 实例搜索逻辑、分类筛选、详情渲染、收藏状态更新全挤在index.js里。结果是data字段超 80 行、onLoad嵌套三层Promise、setData频繁触发导致列表卡顿。本项目采用功能驱动的页面拆分 数据服务层隔离策略pages/category/list纯分类导航页只负责展示一级分类川菜、粤菜等不处理任何菜品数据pages/recipe/list菜品列表页接收category_id参数调用service/recipe.js获取该分类下菜品支持下拉刷新与上拉加载pages/recipe/detail详情页独立处理图片预加载、步骤动画、收藏状态同步service/目录下封装recipe.js数据获取、storage.js本地缓存读写、search.js关键词匹配算法三个模块全部导出 Promise 化方法与页面逻辑解耦。提示service/storage.js中的setFavorite方法会自动合并新旧收藏 ID 数组并去重避免多次点击收藏导致重复 IDgetFavoriteList则返回 Promise.resolve() 包裹的数组确保调用方无需判断wx.getStorage是否异步完成。2.2app.js里的全局初始化逻辑不只是onLaunchapp.js不是摆设。本项目在此完成了三项关键初始化基础库版本校验检查wx.getSystemInfoSync().SDKVersion是否 ≥3.4.0若不满足则wx.showModal提示用户升级客户端并禁用依赖新 API 的功能如wx.onMemoryWarning本地缓存预热在onLaunch中调用storage.init()尝试从wx.getStorage读取recipe_cache和favorite_list失败则设为空对象/空数组避免后续页面首次访问时undefined报错全局事件总线注入通过this.globalData.eventBus new EventEmitter()创建实例供跨页面通信如详情页收藏成功后通知列表页刷新收藏图标。// app.js 片段 App({ onLaunch() { const systemInfo wx.getSystemInfoSync(); if (this._checkSDKVersion(systemInfo.SDKVersion) false) { wx.showModal({ title: 提示, content: 当前微信版本过低请升级至最新版以获得完整体验, showCancel: false, confirmText: 我知道了 }); } this.globalData.storage.init(); // 触发缓存预热 }, _checkSDKVersion(version) { // 语义化版本比较支持 3.4.5 3.4.0 const [a1, a2, a3] version.split(.).map(Number); const [b1, b2, b3] 3.4.0.split(.).map(Number); return a1 b1 || (a1 b1 a2 b2) || (a1 b1 a2 b2 a3 b3); } });这段代码的关键在于_checkSDKVersion的实现——它不是简单字符串比对3.10.0 3.4.0会返回 true而是逐段转为数字比较确保逻辑严谨。如果你直接复制网上“版本号字符串比较”代码这里就会翻车。2.3project.config.json的隐藏配置项让真机调试少走三天弯路很多人忽略project.config.json里的setting字段其实它控制着编译行为。本项目明确设置了以下几项es6: false关闭 ES6 转 ES5因基础库 3.4 已原生支持const/let/箭头函数开启反而增加包体积enhance: true启用增强编译使this.setData支持深层路径如list[0].name避免手动JSON.parse(JSON.stringify())深拷贝preloadBackgroundData: true开启后台数据预加载当用户从聊天窗口跳转到小程序时onLoad中的数据请求已在后台发起首屏更快minified: true强制压缩 WXML/WXSS减少包体积实测主包从 1.2MB 降至 980KB。{ description: 项目配置文件, setting: { urlCheck: true, es6: false, enhance: true, preloadBackgroundData: true, minified: true, newFeature: true }, compileType: miniprogram, libVersion: 3.4.5, appid: wx1234567890abcdef, projectname: caipu-damo, condition: { ... } }注意appid字段值为占位符实际使用时需替换为你自己的 AppIDlibVersion必须与app.json中requiredBackgroundModes等特性兼容本项目libVersion: 3.4.5对应requiredBackgroundModes: [audio]已移除新版不再需要避免审核被拒。3. 数据层设计本地 JSON 缓存策略 搜索算法不依赖后端也能跑通全流程3.1data/recipe.json的结构设计为什么用嵌套数组而不是扁平 ID 映射本项目数据源为data/recipe.json共 327 道菜非随机生成而是按真实烹饪逻辑组织{ categories: [ { id: 1, name: 川菜, icon: icon-chuan }, { id: 2, name: 粤菜, icon: icon-yue } ], tags: [ { id: 101, name: 下饭, color: #FF6B6B }, { id: 102, name: 快手, color: #4ECDC4 } ], recipes: [ { id: 1001, title: 鱼香肉丝, category_id: 1, tag_ids: [101, 102], cover: /images/recipe/1001.jpg, steps: [ { step: 1, desc: 猪肉切丝用料酒、淀粉腌制10分钟, img: /images/steps/1001_1.jpg } ], ingredients: [猪肉, 木耳, 胡萝卜, 青椒] } ] }这种结构的优势在于✅分类与菜品强绑定category_id直接关联categories数组索引列表页filter时无需二次查表✅标签可多选且带样式tag_ids是数组tags中定义颜色渲染时直接tags.find(t t.id tag_id)即可✅步骤与食材分离存储避免recipes数组体积膨胀steps和ingredients可按需懒加载。注意data/recipe.json文件大小为 1.8MB微信要求主包 ≤ 2MB因此未将所有菜品封面图放入主包而是通过cloud://协议引用云存储见 4.2 节确保主包合规。3.2service/recipe.js的缓存策略三级缓存链如何降低白屏率单纯wx.request每次都拉 JSON用户划动列表时会明显感知网络延迟。本项目实现三级缓存缓存层级存储位置生效条件失效策略L1内存缓存app.globalData.recipeData页面间共享JSON.parse后直接引用onHide时不清除onShow时检查时间戳L2本地缓存wx.setStorage({ key: recipe_cache, data })首次加载后写入有效期 24 小时Date.now() - cacheTime 24 * 60 * 60 * 1000L3网络兜底wx.requestL1/L2 均失效时触发HTTP 200 且data.length 0才写入 L2// service/recipe.js async function loadRecipes() { // L1内存中已有直接返回 if (getApp().globalData.recipeData) { return Promise.resolve(getApp().globalData.recipeData); } // L2检查本地缓存 try { const cache await wx.getStorage({ key: recipe_cache }); if (cache.data Date.now() - cache.time 24 * 60 * 60 * 1000) { getApp().globalData.recipeData cache.data; return Promise.resolve(cache.data); } } catch (e) { // 缓存不存在或过期走网络 } // L3网络请求 const res await wx.request({ url: https://example.com/data/recipe.json, method: GET, header: { content-type: application/json } }); if (res.statusCode 200 res.data Array.isArray(res.data.recipes)) { getApp().globalData.recipeData res.data; wx.setStorage({ key: recipe_cache, data: { data: res.data, time: Date.now() } }); } return Promise.resolve(getApp().globalData.recipeData); }关键点wx.setStorage写入的是{ data, time }对象而非原始 JSON这样time字段可用于精确计算过期时间避免wx.getStorageInfo查总大小的不可靠方案。3.3service/search.js的模糊匹配算法不只是includes()小程序没有浏览器的indexOf全局搜索能力input.value.includes(keyword)在中文场景下效果极差“红烧”匹配不到“红烧排骨”。本项目采用分词 权重打分策略对菜品标题、食材、步骤描述分别提取关键词用空格、顿号、逗号分隔计算每个关键词与搜索词的最长公共子序列LCS长度占比标题匹配权重 0.5食材 0.3步骤 0.2最终得分 Σ(单个关键词得分 × 权重)仅返回得分 ≥ 0.3 的菜品。// service/search.js function calculateScore(title, keyword) { const words title.split(/[,、\s]/).filter(w w.length 0); let maxScore 0; for (const word of words) { const lcsLen longestCommonSubsequence(word, keyword); const score lcsLen / Math.max(word.length, keyword.length); maxScore Math.max(maxScore, score); } return maxScore; } function longestCommonSubsequence(str1, str2) { const dp Array(str1.length 1).fill(null).map(() Array(str2.length 1).fill(0)); for (let i 1; i str1.length; i) { for (let j 1; j str2.length; j) { if (str1[i - 1] str2[j - 1]) { dp[i][j] dp[i - 1][j - 1] 1; } else { dp[i][j] Math.max(dp[i - 1][j], dp[i][j - 1]); } } } return dp[str1.length][str2.length]; }这个 LCS 实现虽非最优O(mn) 时间复杂度但对 327 条菜品、平均标题长度 8 字的场景搜索响应时间稳定在 12ms 内远优于正则全局匹配的 80ms。如果你把keyword设为“糖醋”它能命中“糖醋排骨”“糖醋里脊”但不会误伤“红烧肉”。4. 关键交互实现搜索防抖、图片懒加载、收藏状态同步4.1 搜索框防抖与实时建议bindinput不是万能的很多教程直接给input绑定bindinput每次输入都触发wx.request结果用户还没输完“宫保鸡丁”刚打到“宫保”就发了 3 次请求。本项目采用双通道防抖主通道bindinput触发debounceSearch延迟 300ms 执行搜索用户停止输入后辅助通道bindconfirm键盘搜索键立即执行不防抖建议列表bindfocus时加载最近搜索的 5 个关键词存于wx.getStorage(search_history)点击即填充输入框。// pages/recipe/list.js Page({ data: { searchValue: , searchResults: [], showSuggestions: false, history: [] }, onInput: util.debounce(function(e) { const value e.detail.value.trim(); this.setData({ searchValue: value }); if (value.length 0) { this.search(value); } else { this.setData({ searchResults: [] }); } }, 300), onConfirm: function(e) { const value e.detail.value.trim(); if (value.length 0) { this.addToHistory(value); this.search(value); } }, search: async function(keyword) { const results await searchService.fuzzySearch(keyword); this.setData({ searchResults: results }); }, addToHistory: function(keyword) { try { const history wx.getStorageSync(search_history) || []; const index history.indexOf(keyword); if (index -1) history.splice(index, 1); // 移除旧位置 history.unshift(keyword); // 插入到开头 if (history.length 5) history.pop(); // 限制最多 5 条 wx.setStorageSync(search_history, history); this.setData({ history }); } catch (e) { console.warn(保存搜索历史失败, e); } } });util.debounce是自定义工具函数位于utils/util.js它比setTimeout更可靠能清除前一次定时器且支持immediate参数本项目设为false。如果你直接抄网上的setTimeout防抖没做clearTimeout就会出现“输入‘红烧’后删掉仍触发‘红烧’搜索”的玄学问题。4.2 封面图懒加载与错误降级image的 3 层容错小程序image组件默认不支持懒加载但可通过wx.createIntersectionObserver实现。本项目为pages/recipe/list中的每个菜品卡片图片添加观察器初始src设为空字符串>!-- pages/recipe/list.wxml -- view wx:for{{recipeList}} wx:keyid classcard image >// pages/recipe/list.js onImageLoad(e) { const dataset e.currentTarget.dataset; const index this.data.recipeList.findIndex(r r.id dataset.id); if (index -1) { const newList [...this.data.recipeList]; newList[index].loaded true; this.setData({ recipeList: newList }); } }, onImageError(e) { const dataset e.currentTarget.dataset; const index this.data.recipeList.findIndex(r r.id dataset.id); if (index -1) { const newList [...this.data.recipeList]; newList[index].error true; // 触发 WXML 中的 error 状态样式 this.setData({ recipeList: newList }); } }WXML 中通过wx:if{{!item.error}}控制图片显示wx:else渲染灰色占位块。这种写法比wx:elif更轻量且避免hidden属性导致的布局重排。4.3 收藏状态同步wx.setStorage与EventChannel的组合技收藏按钮点击后需同时更新本地缓存中的favorite_list数组当前页面recipe对象的is_favorited字段若从列表页跳转来还需通知列表页刷新对应卡片图标。本项目用EventChannel解决跨页面通信详情页点击收藏 → 调用storage.toggleFavorite(recipe.id)→ 写入缓存然后this.getOpenerEventChannel().emit(favoriteChanged, { id: recipe.id, status: !recipe.is_favorited })列表页在onLoad中监听this.getOpenerEventChannel().on(favoriteChanged, (data) { ... })找到对应菜品更新is_favorited。// pages/recipe/detail.js toggleFavorite() { const { recipe } this.data; storage.toggleFavorite(recipe.id).then(() { const newStatus !recipe.is_favorited; this.setData({ recipe.is_favorited: newStatus }); // 通知上一页如果存在 const eventChannel this.getOpenerEventChannel(); if (eventChannel) { eventChannel.emit(favoriteChanged, { id: recipe.id, status: newStatus }); } }); }注意getOpenerEventChannel()仅在wx.navigateTo传入events时有效本项目pages/recipe/list.wxml中的跳转写法为navigator url/pages/recipe/detail?id{{item.id}} open-typenavigate hover-classnone因此详情页必须通过onLoad的options参数获取id再用eventChannel反向通信——这是微信官方推荐的跨页状态同步方式比wx.setStorageSync全局轮询更高效。5. 避坑指南那些让你调试到凌晨三点的“小问题”5.1 现象真机扫码空白页开发者工具一切正常原因app.json中usingComponents引用了未安装的 npm 包如weui-miniprogram而真机不支持npm构建只认miniprogram_npm目录。开发者工具开启了“使用 npm 模块”但真机环境未执行npm install构建 npm。解决打开微信开发者工具 → 顶部菜单栏「工具」→ 「构建 npm」勾选「使用 npm 模块」点击「构建」。确认miniprogram_npm目录生成且非空。本项目已移除所有 npm 依赖全部使用原生组件故无需此步——但你若自行添加van-button等组件必须执行。5.2 现象搜索“豆腐”能搜到“豆”却搜不到fuzzySearch返回空数组原因service/search.js中longestCommonSubsequence函数对单字词处理有缺陷当str1豆腐、str2豆时LCS 长度为 1但score 1 / Math.max(2,1) 0.5应命中但若str1豆制品、str2豆LCS1score1/3≈0.33仍达标。真正的问题是calculateScore中words分割逻辑豆制品.split(/[,、\s]/)返回[豆制品]未进一步拆分为单字。解决在calculateScore中增加单字 fallbackif (words.length 0 || words[0].length 2) { // 对短词直接按字拆分 words Array.from(str1); }5.3 现象iOS 真机上图片加载缓慢Android 正常原因iOS 微信客户端对https图片证书校验更严格若你的云存储域名如cloud.xxxx.comSSL 证书过期或为自签名iOS 会静默失败。开发者工具用的是桌面 Chrome 内核校验宽松。解决本项目封面图使用微信云开发cloud://协议如cloud://caipu-12345.6361-caipu-12345/cover/1001.jpg由微信统一托管证书彻底规避此问题。切勿替换成自己的 HTTPS 域名除非你确认其证书链完整且有效期 1 年。5.4 现象wx.setStorage报错 “the data is too large to set”但JSON.stringify(data).length只有 1.2MB原因wx.setStorage限制的是序列化后的字符串长度而非对象属性数。当data中包含大量重复字符串如 327 道菜的cover字段都是/images/cover/xxx.jpgV8 引擎序列化时会生成冗余字符串。解决本项目storage.js中setFavorite方法只存id数组如[1001,1002]长度 1KBrecipe_cache存整个 JSON但已通过wx.getStorageInfo校验const info await wx.getStorageInfo(); if (info.currentSize JSON.stringify(data).length info.limitSize * 0.8) { wx.showToast({ title: 缓存空间不足, icon: none }); return; }5.5 现象wx.previewImage在部分安卓机型上黑屏原因urls数组中存在null或空字符串微信底层 SDK 解析失败。本项目pages/recipe/detail.wxml中previewImage绑定的urls来自recipe.steps.map(s s.img)若某步骤无图s.img为undefinedmap后产生null。解决在setData前过滤const urls recipe.steps .map(s s.img) .filter(url url typeof url string url.length 0); this.setData({ previewUrls: urls });6. 进阶技巧用wx.cloud.downloadFile替换cloud://协议实现灰度发布与 A/B 测试6.1 为什么不能永远依赖cloud://协议cloud://是微信云开发的快捷协议写起来爽但有两个硬伤❌无法灰度所有用户看到的都是同一份云存储文件你想给 10% 用户推新版菜谱图做不到❌无法埋点image srccloud://xxx.jpg加载失败时你收不到任何错误日志无法定位是 CDN 故障还是文件误删。本项目预留了service/cloud.js模块提供downloadRecipeImage方法用wx.cloud.downloadFile替代cloud://// service/cloud.js async function downloadRecipeImage(fileID) { try { const res await wx.cloud.downloadFile({ fileID }); if (res.fileContent) { // 将 ArrayBuffer 转为 base64仅用于小图大图用临时路径 const base64 wx.arrayBufferToBase64(res.fileContent); return data:image/jpeg;base64,${base64}; } else { // 返回临时文件路径适用于大图 return res.tempFilePath; } } catch (err) { console.error(云文件下载失败, fileID, err); // 上报错误到自建监控 reportError(cloud_download_fail, { fileID, errMsg: err.errMsg }); return null; } }6.2 如何用downloadFile实现灰度发布假设你要上线新版封面图但只对version 1.2.0且city 深圳的用户开放在云函数getRecipeCover中根据event.userInfo的version和city返回不同fileID小程序端调用wx.cloud.callFunction({ name: getRecipeCover, data: { id: 1001 } })拿到fileID后再调用downloadRecipeImage(fileID)成功后setData({ cover: tempFilePath })。这样你就能在云函数里写// 云函数 getRecipeCover exports.main async (event) { const { id } event; const { version, city } event.userInfo; if (version 1.2.0 city 深圳) { return { fileID: cover_v2_${id}.jpg }; // 新版 } else { return { fileID: cover_v1_${id}.jpg }; // 旧版 } };6.3 A/B 测试收藏按钮文案从“收藏”到“加入我的菜谱本”你想测试“加入我的菜谱本”是否比“收藏”点击率更高不用发两个版本。在pages/recipe/detail.wxml中button bindtaptoggleFavorite {{ abTestText }} /buttonabTestText由云函数动态返回// 云函数 getABText exports.main async (event) { const userId event.userInfo.openId; // 简单哈希分流openId 字符串长度为奇数 → A组偶数 → B组 const group userId.length % 2 0 ? A : B; return { text: group A ? 收藏 : 加入我的菜谱本, group }; };然后在页面onLoad中wx.cloud.callFunction({ name: getABText }).then(res { this.setData({ abTestText: res.result.text }); // 上报分组信息用于后续数据分析 analytics.track(ab_group_assigned, { group: res.result.group }); });这套机制不需要改任何编译配置不增加包体积所有逻辑在云函数和运行时完成。从那以后我每次要做 UI 变更都强制走一遍云函数 A/B 分流 错误监控上报 本地缓存 fallback 三件套——哪怕只是改个按钮文字也要确保线上可灰度、可回滚、可归因。希望帮到你。本文还有配套的精品资源点击获取