ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

前后端分离健康菜谱生成系统:Node.js与Vue全栈开发实践

前后端分离健康菜谱生成系统:Node.js与Vue全栈开发实践 从标题看这是一套前后端分离的个人健康菜谱生成系统技术栈锁定在 Node.js 和 Vue。这个组合在当下的全栈项目里非常典型Node 负责接口层Vue 负责页面交互中间通过 RESTful API 通信。既然是“生成系统”说明它不是简单的菜谱展示而是具备按条件筛选、组合、推荐菜谱的逻辑能力。这篇文章我会把整个项目从环境搭建到前后端联调完整拆解一遍重点放在核心功能如何落地以及我在实际开发中踩过的那些坑希望给正在做类似毕业设计或者个人项目的朋友一些可复用的参考。1. 项目核心定位与功能设计1.1 需求分析与功能地图健康菜谱生成系统关键在“健康”和“生成”两个词。普通菜谱应用只是把菜谱列表按分类展示出来用户自己翻着找。生成系统则需要输入条件系统根据条件自动匹配出符合要求的菜谱组合比如“低脂、高蛋白、不超过500大卡、两人份”系统返回一套完整的早中晚餐方案。我最初接到这个需求时把核心功能拆成了三大块菜谱管理维护菜品基础数据包括食材、用量、热量、蛋白质/脂肪/碳水含量、烹饪方式、口味标签、适合餐次早/中/晚。健康规则引擎这是“生成”的灵魂根据用户设定的目标减脂、增肌、控糖、日常均衡结合食材营养数据筛选出符合条件的菜谱。个性化推荐与组合根据用户性别、年龄、身高体重、活动量估算每日热量需求再按早中晚餐的供能比从菜谱库中匹配组合形成一整天的健康饮食方案。技术选型上我做了这样的取舍后端用 Node.js Express数据库用 MySQL前端用 Vue 3 Vite Element Plus前后端分离部署。没有上 Nest.js 这类重框架因为项目规模不大、功能边界清晰Express 足够灵活路由中间件写起来简单排查问题也更直接。前端也没用 Nuxt纯 SPA 就满足需求后期如果需要 SEO 再考虑改造。1.2 技术选型背后的理由选择 Node.js 不是因为“热门”而是因为它和前端技术栈同源JavaScript 一门语言通吃全栈。对于个人开发者来说这个特点极大降低了上下文切换成本。后端接口层只需要处理 JSON 数据不涉及复杂的 CPU 密集计算Node 的异步 I/O 模型在这种场景下非常合适。MySQL 的选择也很务实菜谱和营养数据有明显的结构化特征菜品名称、分类、营养数值、标签天然适合关系型模型。而且 MySQL 对中文支持好部署维护资料多发现问题好搜解决方案。有人会问为什么不用 MongoDB菜谱数据结构看起来是文档型的但实际开发中“菜品 - 食材 - 营养数据”之间有很多关联查询用关系型数据库写 JOIN 比在 NoSQL 里手工维护引用关系要直观得多。Vue 3 组合式 API 的引入是前端方案的关键决策。菜谱生成页面的交互状态很多用户选择的健康目标、膳食限制、忌口食材、用餐人数这些状态分散在多个组件里。组合式 API 配合响应式对象可以非常清晰地把这些交互逻辑聚合在一个 composable 函数中管理比 Options API 的 data 散落写法更利于维护。2. 环境准备从零搭建开发环境2.1 Node.js 安装与环境配置这个项目的第一步是安装 Node.js但就是这一步拦住不少人。我在网上看到大量搜索记录都卡在“nodejs 安装及环境配置”“npm 无法加载文件 npm.ps1”这类问题上。Node.js 的安装建议直接去官网下载 LTS 版本不要用最新版。LTS 版本经过长时间稳定性验证各种开源依赖兼容性最好。Windows 下建议使用.msi安装包它会自动配置 PATHmacOS 用.pkg包或者 Homebrew 都行Linux 下如果用 apt 装版本往往偏旧建议用 NodeSource 源或者 nvm 管理版本。安装完成后务必验证环境是否正常。Windows 用户打开 PowerShell输入node -v npm -v如果出现“无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本”的报错原因是 PowerShell 的执行策略默认是 Restricted禁止运行任何.ps1脚本文件。解决办法是管理员权限打开 PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned这条命令允许本地脚本运行但远程下载的脚本需要数字签名。设置之后 npm 命令就可以正常执行了。我当时第一次遇到这个报错还以为是 node 装坏了重新装了三次才发现是 PowerShell 策略问题希望看到这段的朋友少走这段弯路。Linux 用户如果安装后node -v提示找不到命令多半是/usr/local/bin不在 PATH 中或者用源码编译时没配置软链接。建议直接用 nvm 安装它会自动处理好路径问题。2.2 项目初始化与依赖管理环境配好之后开始初始化项目。前后端分离我习惯创建两个目录server放后端代码client放前端代码。后端先初始化 package.jsonmkdir health-recipe-server cd health-recipe-server npm init -y接着安装核心依赖npm install express mysql2 cors body-parser jsonwebtoken npm install nodemon --save-dev这里额外说明几个依赖的作用cors是解决前端跨域请求的核心中间件开发环境下前端跑在 5173 端口后端跑在 3000 端口没有 CORS 配置前端请求会被浏览器拦截nodemon用于开发时监听代码变化自动重启服务省去手动重启的操作。前端用 Vite 脚手架创建npm create vitelatest health-recipe-client -- --template vue cd health-recipe-client npm install npm install vue-router4 element-plus axios老读者可能注意到我没用 vue-cli 而是用 Vite不是跟风。Vite 基于 ESBuild 的预构建依赖机制冷启动速度比 Webpack 快一个量级对开发体验的提升是实打实的。vue-router4是 Vue 3 对应的路由版本和 Vue 2 时代的 vue-router3 有一系列 API 差异新手容易在这里装错版本导致路由无法生效。依赖装好之后还有一步容易被忽略的配置配置 npm 镜像源。国内开发者直连 npm 官方源安装依赖经常超时。执行npm config set registry https://registry.npmmirror.com这个源是淘宝 npm 镜像的正式域名同步频率高稳定性好项目里所有依赖都能顺利拉取。装完依赖可以在项目里看看node_modules目录里面有几百个文件夹是正常的这只是依赖树的浅层结构。3. 后端接口设计与核心逻辑实现3.1 数据库设计与连接菜谱系统的核心数据结构设计阶段我画了三张表recipe存菜品基本信息ingredient存食材基础数据recipe_ingredient是菜品和食材的关联表。这个模型是经典的三范式设计避免冗余同时也保留扩展性。recipe 表核心字段CREATE TABLE recipe ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 菜名, category VARCHAR(20) COMMENT 分类家常菜/汤羹/凉菜/主食, method VARCHAR(20) COMMENT 烹饪方式炒/炖/蒸/煮/烤/凉拌, flavor VARCHAR(50) COMMENT 口味标签清淡/微辣/酸甜/咸鲜, meal_type VARCHAR(10) COMMENT 适合餐次breakfast/lunch/dinner, calories INT COMMENT 总热量千卡, protein DECIMAL(5,1) COMMENT 蛋白质克, fat DECIMAL(5,1) COMMENT 脂肪克, carbs DECIMAL(5,1) COMMENT 碳水化合物克, steps TEXT COMMENT 步骤描述, image_url VARCHAR(200) COMMENT 图片地址 );后端的数据库连接用连接池方式避免每次请求都重新建连。Express 项目里我封装了一个db.js模块const mysql require(mysql2); const pool mysql.createPool({ host: localhost, user: root, password: yourpassword, database: health_recipe, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); module.exports pool.promise();连接池的参数值得说一下connectionLimit设置为 10意味着同时最多 10 个连接在池中复用。个人项目并发量不大这个数量足够设太高反而浪费内存。pool.promise()返回 Promise 风格的接口这样可以用async/await写异步代码避免回调地狱。3.2 健康评分算法与菜谱推荐逻辑推荐逻辑是整个系统的核心价值所在也是菜谱生成区别于普通列表展示的关键。第一步是计算用户每日总热量消耗TDEE。用的是 Mifflin-St Jeor 公式男性基础代谢 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄 5 女性基础代谢 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄 - 161 每日总消耗 基础代谢 × 活动系数1.2久坐 / 1.375轻度 / 1.55中度 / 1.725高度第二步根据用户目标确定热量缺口或盈余减脂为总消耗的 80%增肌为 115%日常均衡为 100%。第三步是分餐配比。早中晚热量配比按 3:4:3 分配加餐视情况归入早午之间或午晚之间。把每餐热量目标换算成“不超过该值的菜谱”优先选蛋白质密度高的菜因为蛋白质供能比提高有助于维持饱腹感和肌肉量。推荐接口的核心逻辑我做了一个“评分排序”的版本。每道菜按以下规则打分function scoreRecipe(recipe, userNeeds) { let score 0; // 热量匹配度越接近目标热量分越高 const calDiff Math.abs(recipe.calories - userNeeds.mealCalorie); score Math.max(0, 10 - calDiff / 50); // 蛋白质得分 score recipe.protein userNeeds.mealProtein ? 5 : recipe.protein * 0.2; // 脂肪限制 if (recipe.fat userNeeds.mealFat) score 3; // 口味偏好匹配 if (userNeeds.flavorPref.includes(recipe.flavor)) score 2; // 忌口食材排除 if (containsForbiddenIngredient(recipe, userNeeds.forbidden)) { return -Infinity; } return score; }这里的核心思路是先硬性过滤忌口、不适配餐次再按软性指标打分排序。这是推荐系统里最基础也最实用的 rule-based 方案没有机器学习那么高大上但效果稳定、逻辑透明用户能理解为什么推荐了这道菜。3.3 菜谱组合生成算法单菜品推荐做出来后真正的“一整天菜谱”还需要组合算法。组合算法的难点在于约束条件的叠加一餐内主菜主食汤/饮品的搭配全天不重复食材总营养值达标。我的实现思路是贪心算法假设早餐先推 2 个菜品主菜主食午餐推 3 个主菜副菜主食晚餐推 3 个从高分菜品列表出发依次选择并排除已用食材。function generateDailyPlan(userNeeds) { const mealPlans { breakfast: pickRecipes(breakfast, 2, userNeeds, []), lunch: pickRecipes(lunch, 3, userNeeds, []), dinner: pickRecipes(dinner, 3, userNeeds, []) }; return mealPlans; } function pickRecipes(mealType, count, userNeeds, excludedIngredients) { const candidates getRecipesByMealType(mealType); const scored candidates .map(recipe ({ recipe, score: scoreRecipe(recipe, userNeeds) })) .filter(item item.score 0) .sort((a, b) b.score - a.score); const selected []; const usedIngredients new Set(excludedIngredients); for (const item of scored) { if (selected.length count) break; const recipeIngredients getRecipeIngredients(item.recipe.id); if (recipeIngredients.some(ing usedIngredients.has(ing))) continue; selected.push(item.recipe); recipeIngredients.forEach(ing usedIngredients.add(ing)); } return selected; }这个方案简单可解释返回结果比较稳定。实际使用中一个问题是食材去重逻辑可能让候选池枯竭所以我在 sql 查询阶段就尽可能扩大候选集每餐从菜品库中取前 20 道符合条件的菜进行打分保证排序阶段有足够的选择空间。4. 前端页面构建与交互实现4.1 页面结构与路由设计前端的页面设计遵循任务导向用户打开系统第一眼看到的是“我该怎么吃”而不是一堆管理按钮。我设计了五个主要页面首页展示今日推荐套餐概览和健康评分。菜谱库浏览全量菜谱支持分类、口味、热量范围筛选。智能生成用户输入个人数据和健康目标系统生成一天的健康方案。健康档案记录用户的基本信息和饮食偏好。我的收藏用户收藏的菜谱列表。路由配置使用 Vue Router 4关键点在于动态路由和路由守卫的应用const router createRouter({ history: createWebHistory(), routes: [ { path: /, component: HomeView }, { path: /recipes, component: RecipeListView }, { path: /generator, component: PlanGeneratorView, meta: { requiresAuth: true } }, { path: /profile, component: ProfileView }, { path: /recipes/:id, component: RecipeDetailView, props: true } ] }); router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/); } else { next(); } });/recipes/:id是动态路由详情页参数通过route.params.id获取配合props: true可以更规范地传递参数。4.2 核心组件实现健康目标向导智能生成页面是整个系统交互最重的页面我把它拆成了三个子组件UserInfoForm基本信息收集、GoalSelector目标与偏好选择、PlanResultCard结果展示。这里用了 Vue 3 组合式 API 的composable模式把生成逻辑抽成了独立的useHealthPlan.jsexport function useHealthPlan() { const userProfile reactive({ gender: male, age: 25, height: 170, weight: 65, activityLevel: moderate, goal: lose_fat, forbiddenFoods: [], flavorPref: [] }); const planResult ref(null); const loading ref(false); async function generatePlan() { loading.value true; try { const res await api.generateHealthPlan(userProfile); planResult.value res.data; } finally { loading.value false; } } return { userProfile, planResult, loading, generatePlan }; }这个 composable 把状态和业务逻辑聚合在一起组件里只需要调用generatePlan()并渲染结果代码结构非常干净。4.3 菜谱详情页与数据展示菜谱详情页需要展示营养分析这个模块我用了自定义进度条组件来可视化三大营养素占比。Component 设计上用 Vue 的插槽slot实现了一个灵活的NutritionCardtemplate div classnutrition-card div classcard-header slot nametitle默认标题/slot /div div classcard-body slot默认内容/slot /div /div /template插槽的妙处在于组件结构复用内容由父组件自由填充。在菜谱详情页中我用它展示了热量、蛋白质、脂肪、碳水的具体数值和占每日推荐量的比例。在首页中复用了这个组件展示“今日营养总览”不用重新写布局代码。5. 前后端联调与部署实践5.1 API 封装与跨域处理前端发请求我用 axios 做了一层统一封装。封装的核心设计思想是拦截器分层请求拦截器统一添加 token响应拦截器统一处理错误码。const service axios.create({ baseURL: /api, timeout: 10000 }); service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); service.interceptors.response.use( response response.data, error { if (error.response.status 401) { localStorage.removeItem(token); router.push(/); } return Promise.reject(error); } ); export default service;开发环境下前端访问后端需要解决跨域。Vite 的 proxy 配置是最省事的方式在vite.config.js中设置export default defineConfig({ server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } });这个配置的原理是前端请求的/api/recipes会被 Vite 开发服务器代理转发到http://localhost:3000/recipes浏览器视角下请求是发往同源的自然不会触发 CORS。后端的cors中间件留着是为了安全兜底万一有非浏览器端请求直接访问后端也要能兼容。生产环境的跨域处理思路完全不同。我最终把前端dist目录交给 Express 托管这样前后端同源不存在跨域问题。后端加上静态资源托管app.use(express.static(path.join(__dirname, ../client/dist))); app.get(*, (req, res) { res.sendFile(path.join(__dirname, ../client/dist/index.html)); });这个get(*)是 SPA 部署的关键它会把所有非 API 路由请求都指向index.html由前端路由接管后续的页面切换。如果没有这个配置刷新/generator页面时 Express 会返回 404。5.2 生产构建与环境配置后端接口中容易写死的就是数据库配置。我在实际项目里把配置抽到了.env文件中通过dotenv模块读取。DB_HOSTlocalhost DB_USERroot DB_PASSWORDyourpassword DB_NAMEhealth_recipe JWT_SECRETyour_secret_key PORT3000提到环境变量必须要提.env文件不应该提交到 Git 仓库里面包含了密码和密钥泄露后果严重。在.gitignore中加上.env和node_modules这是所有 Node 项目的基本素养。前端构建cd client npm run build构建产物会输出到dist目录。生产部署时服务器上不需要再装前端依赖只需要把dist文件夹和后端代码一起部署即可。我是直接扔在一台 Linux 云服务器上用 PM2 管理 Node 进程。有个 Linux 部署的常见坑需要提醒默认安全组只开放 80 端口后端如果跑在 3000 端口外部无法直接访问。我的方案是后端端口绑定 3000然后 nginx 反向代理server { listen 80; server_name yourdomain.com; location / { proxy_pass http://localhost:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这样所有流量先进 Nginx再转发到 Node 服务。用 PM2 托管进程保证服务挂掉后自动重启生产环境基本不需要人工干预。5.3 常见问题排查速查表我把这个项目从开发到部署遇到的典型问题整理成了一张表覆盖了环境配置、前后端联调、生产部署三个阶段都是 qa 的形式。问题现象可能原因解决方案npm.ps1无法加载脚本PowerShell 执行策略限制管理员权限执行Set-ExecutionPolicy RemoteSignednpm install超时失败网络连接 npm 官方源慢切换镜像源npm config set registry https://registry.npmmirror.com前端请求后端报 CORS 错误开发环境前端 5173 访问后端 3000 跨域Vite proxy 配置/api代理或后端启用 cors 中间件Vue Router 刷新子路由页面 404生产环境 Express 未托管历史路由后端添加app.get(*)指向index.html中文乱码数据库连接字符集配置缺失数据库连接 URL 参数加上charsetutf8mb4接口返回数据正常但页面空白前端错误未捕获或生命周期内未调用接口检查控制台报错确认onMounted中调用数据加载函数冷启动开发服务器非常慢依赖包中存在编译型模块优先使用纯 JavaScript 库避免安装需要 node-gyp 编译的依赖还有一个容易被忽略的问题Windows 开发机上我遇到过几次“端口被占用”凶手经常是上一次开发服务没完全关闭。解决方法很简单找到占用进程强制结束netstat -ano | findstr :3000 taskkill /PID 你看到的进程号 /F还有数据库连接问题也是高频故障。mysql2连接不上时先检查 MySQL 服务是否启动再检查密码和权限是否允许远程连接。我个人有个习惯写接口前先用一个测试脚本单独测通数据库连接这样可以确定问题出在数据库配置还是接口逻辑避免两头排查浪费时间。6. 菜谱数据的整理方案这个系统的价值不只在于代码逻辑数据质量同样重要。一个健康菜谱系统的推荐准不准很大程度取决于菜谱库覆盖面和营养数据是否可靠。我初期数据采用手工录入加官方资料核对的方式。每道菜的食材用量要精确到克根据《中国食物成分表》标准版计算热量和三大营养素含量。这个数据整理过程很枯燥但直接决定了推荐逻辑的效果——营养数据如果错误再好的算法也是白搭。举例说明数据格式的正确性清炒西兰花这道菜食材是西兰花 200 克热量约 68 千卡橄榄油 10 克热量约 90 千卡蒜末适量可忽略不计。总热量 158 千卡蛋白质约 5.6 克脂肪约 10 克碳水约 6 克。这些数据都要如实填入数据库推荐引擎才有准确的判断依据。数据录入还有一个技巧把菜谱的“适用餐次”仔细标注。有些菜只适合出现在早餐比如全麦面包配煎蛋有些菜午晚餐都适合比如红烧鸡腿。生成的方案里如果没有加餐次约束会出现早餐推荐麻辣香锅的诡异结果。我在pickRecipes的第一步就是按 meal_type 过滤确保推荐结果首先符合用餐场景。如果你觉得自己从头录入几百道菜太累可以直接抓取公共菜谱平台的数据但注意两个问题一是营养数据是否完整很多平台只给了热量没给蛋白质和碳水这会让评分逻辑大打折扣二是版权问题个人学习使用问题不大如果作为商业应用发布需要确认数据来源合法性。7. 部署之后的反思与扩展思路系统基础功能上线之后我复盘了整个开发过程。最有价值的设计决策有两处一是把营养评分封装成独立的可测试函数这让推荐逻辑的优化非常方便二是前端状态管理没有过度设计只用了 composable 而没上 Pinia因为当前项目共享状态范围小引入状态管理库反而增加了心智负担。一个值得注意的技术细节是MySQL 查询我用参数化查询防止 SQL 注入。任何用户输入直接拼进 SQL 语句都是高危行为通过?占位符传参是最基础的安全底线。Node 后端项目里我见过太多人为了省事直接拼接字符串这种习惯一旦线上出现注入攻击整个数据库都可能被拖走。对于下一步扩展方向我认真想过几个有价值的功能点基于用户饮食记录的口味学习和菜谱偏好建模让推荐结果逐步个性化从“通用健康”进化到“千人千面”。对接更多食材数据库打通购物清单生成功能把推荐的菜谱自动转换成生鲜电商的可下单清单。移动端适配优化。目前前端布局在手机上可用但操作体验还需要优化可以考虑做 PWA 离线访问。再分享一个我在实际使用中的数据洞察用户对“忌口食材”功能的反馈最好。很多菜谱系统推的菜本身是健康的但用户可能海鲜过敏、不吃香菜、或者正在忌口辛辣。把这类硬性约束放在推荐的最前面比任何营养算法都更能让用户感觉到智能。如果你正准备复刻这个项目我的建议是不要一开始就追求功能多先把“菜谱管理 健康数据录入 按条件筛选 组合推荐”这条主链路打通验证核心价值是否成立再考虑收藏、评论、分享这类锦上添花的功能。骨架稳固之后往上加肉就容易多了。
返回列表