简介:微信小程序开发常受困于服务器搭建、域名备案等基础设施问题,云开发将数据库、存储和云函数整合为开箱即用的后端方案,让开发者专注于业务逻辑。其原理是通过云函数在服务端读写云数据库,小程序端仅调用接口,既保证数据安全又简化权限管理。这种架构尤其适合内容展示型应用,如菜谱小程序这类读多写少的场景,可快速实现菜谱列表、详情、收藏和搜索功能。本文基于一份云开发菜谱小程序源码,讲解工程结构、云环境配置、数据库集合设计、云函数部署全流程,并总结主包超限、真机图片失效、冷启动超时等高频避坑经验,帮助个人开发者从零跑通上线。
1. 拿到的这份菜谱小程序源码:先看清楚它省掉了哪些事
把这份菜谱微信小程序源码从 .rar 压缩包里解压出来,你大概率会看到两个顶层目录:miniprogram和cloudfunctions。前者跑在小程序端,后者跑在云开发的 Node.js 运行时里。整个项目不依赖自购服务器,不需要域名备案和 HTTPS 证书配置,前后端共用同一套账号体系——这是当下个人做微信小程序最省事的路线之一。
这类源码解决的核心问题是「一个人把一个完整产品从 0 推到上线」。云开发把数据库、文件存储、云函数三项最重的后端能力打包成开箱即用的服务,你只需要关注页面和业务逻辑。它的目标用户很明确:没有后端团队的个人开发者、想快速验证菜谱类产品原型的内容创业者,以及手里已有菜谱资源想做成小程序分发的美食博主。
2. 云开发和源码目录:为什么菜谱类小程序最吃这套架构
2.1 为什么我不建议给菜谱项目自建服务器
菜谱类小程序是典型的读多写少应用:用户高频浏览菜品列表、详情、步骤,但写入行为只有收藏、浏览记录、评论等低频操作。这种负载特征决定了它对后端的需求不是「强计算」,而是「够用的数据存取 + 快速上线」。
如果我按传统方案来做,需要买一台云主机或轻量服务器、注册域名、做 ICP 备案、配 Nginx、装 MySQL 或 MongoDB、再写一套鉴权接口。这里面每一项都不是写业务代码,而是铺基础设施。对一个以内容展示为主的菜谱项目来说,这套流程要消耗的时间往往比写小程序页面本身还多。云开发把这几件事合并成一次开通操作:你在微信开发者工具里点一下创建环境,数据库、存储、云函数运行时就都有了。
菜谱项目尤其适合云开发还有一个原因:菜谱数据天然以「图片 + 结构化文本」为主。云存储专门管理图片文件,云数据库存结构化字段,云函数处理分页和聚合查询。三者的组合恰好覆盖一道菜从封面、食材、步骤到收藏统计的全部数据形态。此外,云开发按调用量和存储量计费,冷启动阶段几乎花不了多少钱。
2.2 源码包里的两个世界:miniprogram 与 cloudfunctions
常见的云开发源码包会把工程按功能拆分归档,目录结构大致如下:
菜谱小程序源码/ ├── miniprogram/ │ ├── app.js │ ├── app.json │ ├── pages/ │ │ ├── index/ # 首页:轮播、分类入口、推荐列表 │ │ ├── category/ # 分类页:按菜系/场景筛选 │ │ ├── detail/ # 详情页:食材、步骤、收藏按钮 │ │ ├── search/ # 搜索页:按菜名/标签搜索 │ │ ├── favorite/ # 收藏页:我的收藏列表 │ │ └── profile/ # 个人中心:登录态、绑定手机号 │ ├── components/ │ │ ├── recipe-card/ # 菜谱卡片组件 │ │ └── search-bar/ # 搜索栏组件 │ └── utils/ │ └── request.js # 封装云函数调用 ├── cloudfunctions/ │ ├── getRecipes/ # 菜谱列表(分页、分类过滤) │ ├── getRecipeDetail/ # 菜谱详情(浏览量+1) │ ├── collectRecipe/ # 收藏/取消收藏 │ └── login/ # 登录态处理 └── project.config.json这个结构的核心设计是「端」与「云」分离。miniprogram目录里的代码最终会被微信开发者工具编译成小程序包,里面不应该出现数据库连接串、密钥等敏感信息;cloudfunctions目录下每个子目录都是一个独立的云函数,微信开发者工具分别打包上传到云端,以 Node.js 函数形式运行。
对比一下就能看出选型逻辑:菜谱数据表、图片文件都放在云开发侧,小程序端只关心「调用哪个云函数、拿到什么结构的数据」。如果以后想把数据迁移到自己的服务器,只需要把云函数内部替换成 HTTP 请求,页面代码几乎不用动。
2.3 一次完整的云开发数据流
整个小程序的请求链路可以浓缩成一个循环:页面触发事件 → 小程序端调用云函数 → 云函数读写云数据库/云存储 → 返回数据 →setData渲染。
// miniprogram/utils/request.js 中常见的云函数调用封装 const callFunction = (name, data = {}) => { return new Promise((resolve, reject) => { wx.cloud.callFunction({ name: name, // 云函数名称,需与 cloudfunctions 下的目录名一致 data: data, // 业务参数,例如 { categoryId, page, pageSize } success: res => resolve(res.result), fail: err => reject(err) }); }); }; module.exports = { callFunction };这里有个容易误解的点:wx.cloud.callFunction返回的res.result才是云函数的业务返回,而res.result外层还包着一层云开发框架的固定结构。新手经常直接拿整个res去渲染,导致页面拿不到数据。只要在success回调里先取.result,后面所有页面的代码才能保持统一。
这种「页端不直连数据库」的约束很关键。云数据库的客户端 API 虽然可以在小程序里直接用,但会把集合权限暴露给前端,一旦权限配置稍有不慎,任何人都能读到或篡改数据。通过云函数收口,所有读写逻辑都落在云端代码里,前端拿到的只是接口返回的视图数据。
2.4 页面与组件是如何组织的
源码里如果按组件化思路组织,首页一般会拆成三块:顶部搜索栏、分类导航区、推荐菜谱列表。搜索栏是独立组件,因为它在搜索页和首页都会被复用;菜谱卡片也是独立组件,用于首页、分类页、收藏页三处,保证同一种视觉呈现只维护一份代码。
app.json里注册的 tabBar 通常包含首页、分类、收藏、个人中心四个页签。选择这四个入口的理由很直接:首页承载发现和推荐,分类页解决目标明确的查询,收藏页沉淀用户行为,个人中心承载登录和账号信息。搜索功能从体验上不该做成 tab,而是做成首页顶部的入口,用户点进去再跳转到搜索页。
页面间的跳转则遵循小程序后台栈的规则:从首页进入详情页用wx.navigateTo,详情页返回首页可以维持原页面状态;如果在个人中心未登录状态下点收藏,需要先弹登录引导,这要求在收藏按钮的点击事件里做登录态判断,而不是让请求发到云端才报错。
3. 把「云开发菜谱小程序」跑通:导入、建库、部署云函数的完整步骤
3.1 跑通前的四个前置条件
动手之前先把环境对齐,避免中途卡在工具链上。第一,需要一个微信小程序 AppID,云开发不支持测试号,注册小程序是免费的,个人主体即可。第二,安装微信开发者工具稳定版,把它升级到最新稳定版能省去很多基础库兼容问题。第三,机器上要有能解压 rar 的工具,Windows 上我习惯用 Bandizip,macOS 用 The Unarchiver,这类工具能自动识别中文文件名编码,直接用系统自带解压有可能出现乱码。第四,微信开发者工具登录时,确保扫码的微信号是该小程序项目的管理员或开发者,否则导入后无法操作云开发。
前置条件里最容易被忽略的是第四点。很多人拿着源码包导入后一片空白,第一反应是代码有问题,其实是因为扫码登录的微信没有这个项目的开发权限,云开发环境根本不在当前账号名下。
3.2 导入工程并把云环境 ID 对齐
打开微信开发者工具,选择「导入项目」,目录指向解压后包含project.config.json的那一层,填入小程序 AppID。导入完成后的第一件事不是点编译,而是检查云环境 ID 是否一致。云环境 ID 是你在云开发控制台创建环境时生成的唯一标识,格式类似cloud1-xxxx,它同时出现在小程序端和云函数端。
// miniprogram/app.js App({ onLaunch() { if (!wx.cloud) { console.error('当前基础库版本过低,请使用 2.2.3 以上基础库'); return; } wx.cloud.init({ env: 'your-env-id', // 替换成你自己的云环境 ID traceUser: true // 记录用户访问,用于控制台分析 }); } });环境 ID 不一致的表现很典型:编译不报错,但所有云函数调用都在fail回调里返回环境不存在。修改时要检查两个位置:app.js里的wx.cloud.init,以及云函数代码里如果显式指定了env,同样要替换。我个人习惯云函数里不写env,让它默认运行在部署时指定的环境下,这样换环境只需要在控制台切换部署目标。
3.3 建集合与导入菜谱数据
云开发控制台左侧「数据库」标签页里创建集合。菜谱项目至少需要这几个集合:recipes(菜谱主表)、categories(分类)、favorites(收藏)、users(用户信息),首页如果有轮播图则再加一个banners。创建集合时不需要事先定义字段,云数据库是文档型,每条记录可以有不同的字段,这对菜谱这种字段经常微调的业务很友好。
导入菜谱数据有现成两条路。第一种是在控制台手动创建记录,适合验证阶段只填三五条;第二种是用 JSON 文件批量导入,适合从其他平台迁移数据。批量导入时控制台对文件大小有限制,我一般把数据切成每个文件几百条再分批导入,避免一次导入失败整批回滚。
// recipes.json 导入格式示例(数组内每个对象是一条记录) [ { "name": "可乐鸡翅", "categoryId": "meat", "tags": ["家常菜", "下饭", "甜口"], "cover": "cloud://your-env-id.xxx/cover/kele-jichi.jpg", "ingredients": [ { "name": "鸡翅中", "amount": "10个" }, { "name": "可乐", "amount": "330ml" } ], "steps": [ { "text": "鸡翅两面煎至金黄", "pic": "" }, { "text": "倒入可乐没过鸡翅,加生抽和老抽", "pic": "" } ], "cookTime": 30, "difficulty": 1, "views": 0, "createTime": "2024-01-01T12:00:00.000Z" } ]导入时注意_id字段不要自己填,让系统生成,否则后续详情页按_id查询时容易和已有的 ID 冲突。createTime建议用 ISO 字符串格式而不是时间戳,因为控制台导出再导入时,时间戳字段会被转成字符串,格式一旦混用,按时间排序就乱了。cover字段如果暂时没有云存储文件,可以先填一个空字符串,等图片传上去再更新,不要因为它导致导入失败。
3.4 部署云函数与真机预览
数据到位后再部署云函数。在微信开发者工具的资源管理器里找到cloudfunctions目录,对每个云函数子目录右键,选择「上传并部署:云端安装依赖」。这个选项会把该目录下的 Node.js 代码连同依赖一起打包上传到云端,比「上传所有文件」更可靠,因为依赖解析发生在云端,不会因为本地 node_modules 版本不一致产生问题。
部署完成后的验证路径是:在开发者工具里编译项目 → 打开首页 → 看云函数列表里是否有调用记录。点开云开发控制台「云函数」页面,能看到每次调用的耗时、返回状态和日志。凡是返回状态为失败的记录,点开日志大概率能直接定位到错误行。
真机预览这一步不能省。开发者工具里的模拟器环境和真机有三处关键差异:扫码预览时使用的并非开发者工具内置的调试基础库、图片加载策略不同、手机网络下云函数冷启动耗时更长。点击开发者工具工具栏的「预览」按钮生成二维码,用小程序管理员微信扫码,在真机上完整走一遍首页 → 详情 → 收藏 → 个人中心的流程。真机上有问题再回来看工具,优先排查上面三个差异点。
4. 数据库设计与核心页面拆解:收藏、搜索、自定义导航栏背后的逻辑
4.1 集合设计:五张表和字段选择
菜谱小程序的数据库设计决定了后续所有功能的扩展空间。recipes是绝对的主表,字段需要覆盖菜谱展示和检索的完整视图:名称、分类 ID、标签数组、封面、食材数组、步骤数组、烹饪时长、难度、浏览量。steps内部每一项同时包含文字说明和可选的步骤图,这个设计比把步骤拼成一个长字符串要灵活得多,以后要做「分步高亮」或「步骤语音播报」时不用改表结构。
categories表只需要三个字段:分类名称、排序值、图标。排序值用于控制分类入口在首页的排列顺序,菜品数量不需要冗余存储,需要展示时可以动态聚合。favorites表是连接用户和菜谱的中间表,核心字段是openid和recipeId,用户维度查询时走openid,菜谱热度统计时走recipeId。
| 集合名 | 主要字段 | 用途 | 权限建议 |
|---|---|---|---|
| recipes | name, categoryId, tags, cover, ingredients, steps, cookTime | 菜谱主内容 | 所有用户可读 |
| categories | name, sort, icon | 分类导航 | 所有用户可读 |
| favorites | openid, recipeId, createTime | 收藏关系 | 仅创建者可读写 |
| users | openid, nickname, avatarUrl | 用户画像 | 仅创建者可读写 |
| banners | imageUrl, link, sort | 首页轮播 | 所有用户可读 |
字段命名有个值得坚持的习惯:所有时间字段统一用createTime、updateTime的 ISO 字符串,不要混用create_date、cTime这类别名。开发的后期你会频繁写聚合统计,统一命名能让查询逻辑少踩很多坑。
4.2 列表接口:分页与分类过滤的参数口径
列表页是最容易写错的云函数,因为分页参数和过滤条件一旦设计得不一致,前端会频繁拿到重复或缺失的数据。常见的做法是把列表云函数设计成统一入口:通过categoryId控制分类过滤,通过page和pageSize控制分页,返回数据里附带total方便前端渲染触底加载。
// cloudfunctions/getRecipes/index.js const cloud = require('wx-server-sdk'); cloud.init(); const db = cloud.database(); const recipes = db.collection('recipes'); exports.main = async (event) => { const { categoryId = '', page = 1, pageSize = 10, keyword = '' } = event; const where = {}; if (categoryId) { where.categoryId = categoryId; } if (keyword) { // 用正则做菜名模糊匹配,仅适用于中小数据量 where.name = db.RegExp({ regexp: keyword, options: 'i' }); } const query = recipes.where(where); const countRes = await query.count(); const listRes = await query .orderBy('createTime', 'desc') .skip((page - 1) * pageSize) .limit(pageSize) .get(); return { total: countRes.total, list: listRes.data, page, pageSize }; };分页这里有两个关键参数:skip表示跳过的记录数,limit表示最多返回多少条。在小数据量下skip + limit是够用的,但skip的数字越大查询越慢,因为数据库要扫描并丢弃前面所有记录。菜谱项目上千条数据以内感觉不到差别,真做到数万条时,要改成基于createTime和_id的游标分页:下一页查询时带上createTime < 上一页最后一条记录的时间条件。
db.RegExp是云数据库的查询语法,服务端执行正则匹配,适合搜索菜名和标签。它和普通正则的区别是必须在db.RegExp里传参数,直接把 JavaScript 字符串拼进where是查不出结果的。搜索接口不建议对每个词都做正则,比如用户输入「鸡翅」时命中菜名即可,标签搜索可以另建搜索索引,或者简单点直接把tags数组的匹配也放进同一条件。
4.3 收藏功能:客户端权限和云函数身份的边界
收藏功能前后端各有一次身份判断。页面端判断用户是否已登录,未登录则弹窗引导去个人中心授权;云函数端判断「当前用户收藏过没有」,决定执行插入还是删除。
这里有一个关键区别:云函数运行时的身份不是用户身份,而是管理员身份。云函数里直接查favorites.where({ openid: 'xxx' })能查到所有用户的收藏记录,不受集合权限限制;但云函数本身并不知道调用者是谁,必须显式从上下文里取。
// cloudfunctions/collectRecipe/index.js const cloud = require('wx-server-sdk'); cloud.init(); const db = cloud.database(); const favorites = db.collection('favorites'); exports.main = async (event) => { const { OPENID } = cloud.getWXContext(); // 从云函数上下文取 openid const { recipeId, action } = event; // action: 'add' 或 'remove' if (action === 'add') { const existed = await favorites.where({ openid: OPENID, recipeId }).get(); if (existed.data.length > 0) { return { code: 0, message: '已收藏过' }; } await favorites.add({ data: { openid: OPENID, recipeId, createTime: new Date().toISOString() } }); return { code: 0, message: '收藏成功' }; } if (action === 'remove') { await favorites.where({ openid: OPENID, recipeId }).remove(); return { code: 0, message: '已取消' }; } };这里最容易翻车的点是直接用wx.cloud.database()在客户端写收藏。客户端的权限模型是「仅创建者可读写」时,查询条件里必须带_openid字段才能命中自己的记录,如果权限误设成「所有用户可读写」,那任何用户都能删掉别人的收藏。所以收藏这类写操作尽量走云函数,权限可以直接设为「仅管理端可读写」,从入口上封死。
收藏状态在详情页需要回显。我的做法是进入详情页时并行调用两个云函数:getRecipeDetail拿菜谱数据、collectRecipe的查询模式拿当前用户的收藏状态。收藏状态不要单独存在本地缓存里,因为用户可能在多台设备上登录,缓存会导致收藏按钮显示与实际不符。
4.4 自定义顶部导航栏的高度计算
菜谱详情页如果想做成沉浸式顶图效果——封面图延伸到状态栏后面,就需要自定义导航栏。自定义导航栏的难点不在视觉,而在精确拿到「状态栏高度」和「胶囊按钮位置」。
// miniprogram/pages/detail/index.js const { windowInfo, menuButton } = getNavigationBarInfo(); function getNavigationBarInfo() { const windowInfo = wx.getWindowInfo(); // 状态栏高度、屏幕宽度等 const menuButton = wx.getMenuButtonBoundingClientRect(); // 胶囊按钮位置 const statusBarHeight = windowInfo.statusBarHeight; const navBarHeight = (menuButton.top - statusBarHeight) * 2 + menuButton.height; return { statusBarHeight, navBarHeight, menuButton }; }导航栏的高度计算公式是业界通用的:状态栏高度 + 胶囊按钮上下留白等于胶囊到状态栏的距离 × 2 + 胶囊高度。直接写死一个44px是典型的翻车做法,因为不同机型的胶囊按钮位置不一致,刘海屏和非刘海屏的状态栏高度差异很大。拿到statusBarHeight后要把它应用到页面的padding-top,把主内容区顶到状态栏下方,同时给自定义导航栏里的返回按钮和标题设置top: statusBarHeight,避免被刘海遮挡。
顶部导航栏高度相关的适配要在真机预览时重点检查。开发者工具模拟器默认的机型参数和真机总有偏差,建议至少用一台刘海屏手机和一台非刘海屏手机交叉验证。还有一点容易忽视:用户从详情页转发小程序给朋友时,转发卡片会复用当前页面的截图,如果导航栏位置错位,转发的卡片图会非常显眼。
5. 实践避坑:云开发版菜谱小程序的 5 个高频翻车现场
5.1 图片资源把主包推到 2612KB,编译直接报 2MB 超限
现象:代码写好后一点编译,控制台报source size 2612kb exceed max limit 2mb,小程序包根本生成不出来。
原因:本地资源目录里塞了大量菜品图片,或者代码里用了 base64 格式的内嵌小图,导致主包体积超出微信限制。小程序主包限制是 2MB,这不是配置项,是平台硬性约束。图片是菜谱类项目里最占体积的资源,一张 800×600 的 JPG 就有 100-200KB,本地塞二十张就撞线。
解决:把图片全部从代码包里挪走,传云存储,页面里用cloud://协议的 fileID 引用。云存储本身不占主包体积,代码包只留文本数据。打包时在project.config.json里确认packOptions.ignore规则,把images等本地资源目录排除出去。如果一个页面确实需要极快的首图展示,最多挑一两张 10KB 以内的占位图放本地,其余全部云端化。
5.2 开发者工具能看到图片,真机上一片空白
现象:模拟器上菜谱封面、步骤图显示正常,一扫码真机预览,图片区域空白,有时候过几秒又自动加载出来,加载时间不固定。
原因:页面里把云存储的图片转成了https://临时链接,再放到<image>组件的src上。云存储生成的临时链接有时效性,开发者在工具里调试时链接有效,真机上如果代码把临时链接缓存起来,过期后就加载失败。另外开发者工具默认不校验域名,真机上对非白名单域名有严格限制,临时链接的域名不在合法域名列表里时会静默失败。
解决:<image>组件的src直接使用cloud://协议的 fileID,微信内置的图片组件能识别并自动拉取。如果业务确实需要https链接,调用wx.cloud.getTempFileURL换取临时链接后立即使用,不要存到变量里复用。排查时打开真机调试的 Network 面板,看图片请求的状态码,404或403都是临链失效的典型特征。
5.3 云函数首次调用超时,列表页 3 秒转圈
现象:首页第一次加载时列表空白,转圈超过 3 秒才出数据;退出小程序重新进入,第二次加载明显快很多。
原因:云函数存在冷启动机制。首次调用时云端要拉起运行时并加载依赖,耗时通常比后续调用多几倍。菜谱列表函数里如果包含了多个await串行查询——先查分类、再查菜品、再查轮播图——整体耗时叠加后就容易触发云函数的默认超时时间。
解决:在云开发控制台「云函数 → 配置」里把超时时间从默认值调整为 20 秒,给冷启动留出余量。同时优化云函数内部逻辑,把互不依赖的查询用Promise.all并行执行。改动代码后要注意,只有重新「上传并部署」才会让配置和新代码同时生效,只改控制台配置不重新部署,逻辑不变,超时依然在旧代码上生效。
5.4 权限开成「所有用户可读写」,收藏表被人恶意刷爆
现象:小程序上线后,favorites集合里出现大量非真实用户的收藏记录,数据量短时间内暴涨,账单费用异常。
原因:数据库权限设置里把favorites集合点成了「所有用户可读写」。客户端 API 在权限宽松时可以被外部脚本绕过页面直接调用,任何人拿到env后都能往集合里写入任意数据,包括伪造的收藏关系。
解决:所有涉及写操作的集合权限一律收紧。favorites、users这类带隐私语义的集合设为「仅管理端可读写」,所有读写都走云函数,云函数以管理员权限运行,不受集合权限限制。recipes、categories这类公开内容设为「所有用户可读」,但不可写。这样配置后,即使前端被逆向,能做的也只有读取公开菜谱,破坏面被控制住了。
5.5 模拟器里能登录,真机上拿不到手机号
现象:开发者工具模拟器里点「获取手机号」按钮,可以正常弹出授权并拿到号码;真机预览时按钮点击后没有反应,或者云函数报错解析不到数据。
原因:手机号获取接口是微信官方的高敏感能力,前提是已认证的小程序 + 真机环境 + 用户主动点击 button 触发。开发者工具模拟器无法完整模拟手机号授权链路,部分源码里用模拟器测试时走了临时放行的逻辑,真机上这个逻辑不生效。另一个常见原因是没有把getPhoneNumber的返回数据传到云函数端用cloud.getOpenData解析,而是试图在小程序端直接解密,解密所需的 session_key 根本拿不到。
解决:页面里用<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhone">来触发,回调里拿到detail.code后传给云函数。云函数内调用cloud.getOpenData({ code })换取真实手机号,这一步必须发生在云端,客户端不能解密。真机和模拟器行为不一致属于平台限制,不要试图在模拟器上验证整条链路,直接扫码真机测。
6. 验证与进阶:把首屏压进 1 秒,再跑通「猜你想做」推荐
6.1 首屏性能:并行请求、懒加载与骨架屏
验证一个菜谱小程序是否「能打」,第一个指标是首屏加载耗时。打开开发者工具的「性能」面板,真机预览状态下点开录制,看首页index页面从启动到首帧渲染的时间线。常见瓶颈集中在三处:云函数串行调用、图片并发请求过多、页面数据未到达时白屏。
性能优化按优先级做三件事:第一,首页的数据请求合并成一个云函数,内部用Promise.all同时查轮播图、分类、推荐列表,把三次网络往返压成一次;第二,列表图片启用懒加载,<image>组件加lazy-load="true",屏幕外的卡片不参与加载;第三,云函数返回前把封面图缩略图化,云存储支持图片处理参数,小程序端只加载裁剪后的缩略图,详情页再加载原图。缩略图的格式类似在 fileID 后拼接处理参数,具体参数名以云存储文档为准,但思路是「列表用小图,详情用大图」,这能显著减少首屏流量。
6.2 用漏斗数据验证菜谱功能是否真的可用
首屏快不等于产品成立。我习惯在recipes集合里维护views字段,并在用户进入详情页时调用云函数自增;同时在收藏表建立后,通过云开发控制台的数据库聚合分析看三个比率:详情页浏览量中点击收藏的比例、搜索页到详情页的转化率、收藏后七天内再次访问的比例。
如果数据量很小,直接在控制台用db.collection('favorites').count()就能知道有多少人收藏过;数据量上来后,看「用户访问来源」更有效——是通过首页推荐进入详情多,还是分类页进入多。菜谱类内容的核心指标是「收藏率」,我见过很多项目浏览量高但收藏率不到 1%,这种情况要回头检查详情页菜谱质量、封面吸引力和步骤清晰度,而不是继续优化加载速度。一个反常识结论是:菜谱小程序的重度用户不看推荐流,他们直接搜索菜名。所以搜索页的接口耗时和结果准确性,权重比首页推荐更高。
6.3 基于收藏标签的「猜你想做」冷启动
当收藏数据积累到几百条后,可以做最简单的推荐:统计当前用户所有收藏菜谱的标签频率,把频率最高的前三个标签作为特征,从全量菜谱里找出包含这些标签但用户还没收藏过的菜品。这个方案不需要任何机器学习依赖,一个云函数就能实现。
// cloudfunctions/recommendRecipes/index.js const cloud = require('wx-server-sdk'); cloud.init(); const db = cloud.database(); const _ = db.command; exports.main = async () => { const { OPENID } = cloud.getWXContext(); // 1. 查当前用户收藏的菜谱 ID const favRes = await db.collection('favorites') .where({ openid: OPENID }) .limit(50) .get(); if (favRes.data.length === 0) { // 没有收藏记录时回退到热门榜单 const hot = await db.collection('recipes') .orderBy('views', 'desc') .limit(6) .get(); return { list: hot.data }; } const favIds = favRes.data.map(item => item.recipeId); // 2. 取收藏菜谱的标签并统计 const favRecipes = await db.collection('recipes') .where({ _id: _.in(favIds) }) .get(); const tagCount = {}; favRecipes.data.forEach(r => { (r.tags || []).forEach(t => { tagCount[t] = (tagCount[t] || 0) + 1; }); }); const topTags = Object.keys(tagCount) .sort((a, b) => tagCount[b] - tagCount[a]) .slice(0, 3); // 3. 根据标签召回,排除已收藏 const recommend = await db.collection('recipes') .where({ tags: _.in(topTags), _id: _.nin(favIds) }) .limit(10) .get(); return { list: recommend.data }; };这个方案的边界要认得清楚:它适合几百到几千条菜谱的冷启动阶段,推荐结果的多样性基本没有,用户连续收藏几道红烧肉后,推荐流里大概率也都是红烧类。做到这个程度后想再进一步,需要给用户建偏好画像集合,记录浏览时长、搜索关键词、收藏时间分布,把标签加权改成带时间衰减的评分模型。但说实话,对大部分菜谱小程序来说,把首页排版、菜谱质量、搜索准确度打磨好,比上一个复杂推荐模型带来的实际收益更大。
我在这类项目里踩过最深的一次坑,是把所有图片堆在本地,上线前一天才发现主包超限,连夜传云存储才救回来。后来养成一个习惯:任何新的菜谱图片,第一时间进云存储,代码里只保留 fileID 引用。这个习惯省下的时间,比我写两套云函数还多。希望这些经验能帮到你,把云开发版菜谱小程序从解压 rar 一路跑到真实用户手里。
本文还有配套的精品资源,点击获取