今天,正式开启一个全新的技术系列 ------ 从零构建一个带 AI 解析、步骤动画、教学演示的全栈烹饪教程微信小程序。
这个项目是一次技术栈的综合实践,也是对工程化开发理念的深度探索。本系列将完整记录从 0 到 1 的构建过程,包括架构设计、前后端联调、AI 功能接入和用户体验优化,希望能为同样在学习路上的开发者带来一些启发。

一、项目定位与开发目标
在动手编码之前,首先需要明确项目的核心定位:
一个集菜谱发布、AI 解析、分步教学、动画演示于一体的社区化烹饪小程序。
基于这个定位,设定了几个核心目标:
结构化菜谱教学:通过 JSON 结构化数据,实现可动态配置的分步教学流程。
沉浸式动画体验:为不同烹饪步骤(切菜、翻炒、蒸煮)匹配专属 GIF 动画,提升教学直观性。
AI 赋能内容生成:用户上传菜谱文案,后端 AI 自动解析为标准化 JSON 数据,降低发布门槛。
工程化模块化架构:采用清晰的目录结构和组件化设计,保证项目可维护、可扩展。
为了实现这些目标,第一步,也是最关键的一步,就是搭建好项目的骨架。
示例代码:菜谱结构化数据模型
javascript
// utils/recipe-model.js
const RecipeSchema = {
id: '',
title: '',
coverImage: '',
description: '',
difficulty: 1, // 1-5 难度等级
cookingTime: 30, // 烹饪时长(分钟)
ingredients: [
{
name: '',
amount: '',
unit: '' // 克、毫升、个等
}
],
steps: [
{
order: 1, // 步骤序号
action: '切菜', // 操作动作
animation: 'cut', // 对应的动画标识
duration: 120, // 预计耗时(秒)
description: '',
tips: '' // 小贴士
}
],
tags: [], // 菜系标签
createdAt: null,
updatedAt: null
}
module.exports = RecipeSchema
二、项目目录结构设计详解
很多新手开发小程序时,会把所有代码都塞进 pages 文件夹,导致后期维护起来像一团乱麻。这次采用了一套高内聚、低耦合的模块化目录设计,结构如下:
plaintext
├── components/ # 可复用的自定义组件
│ ├── ingredient-list/ # 食材清单组件
│ ├── recipy-card/ # 菜谱卡片组件
│ └── step-timeline/ # 步骤时间线组件
├── pages/ # 所有业务页面
│ ├── cook/ # 烹饪发布页
│ ├── kitchen/ # 厨房首页(菜谱列表)
│ ├── login/ # 登录页
│ ├── mine/ # 个人中心页
│ ├── recipy-detail/ # 菜谱详情页
│ ├── recipy-edit/ # 菜谱编辑页
│ ├── square/ # 广场页
│ └── teach/ # 核心教学演示页
├── utils/ # 工具函数与通用模块
│ ├── animation-map.js # 步骤动画映射表
│ ├── auth.js # 登录态与权限校验
│ └── request.js # 全局请求封装
├── images/ # 静态资源文件
│ ├── cook-tab/ # 底部导航栏图标
│ ├── gif/ # 烹饪步骤GIF动画
│ └── tab/ # 底部导航栏图标
├── app.js # 小程序入口文件
├── app.json # 全局配置文件
└── app.wxss # 全局样式文件
下面逐一拆解每个核心目录的设计思路。
1. pages:业务页面的容器
这是小程序的基础,所有页面都必须注册在这里。按照业务模块划分,将首页、广场、发布、个人中心、教学演示等功能拆分成独立页面,每个页面拥有自己的 js/wxml/wxss/json 文件。
优势:业务边界清晰,修改某个页面不会影响其他模块,也方便后续按需分包加载,优化启动速度。
示例代码:页面注册与生命周期
javascript
// pages/kitchen/kitchen.js
Page({
data: {
recipeList: [],
page: 1,
hasMore: true
},
onLoad(options) {
// 页面加载时获取菜谱列表
this.fetchRecipeList()
},
onPullDownRefresh() {
// 下拉刷新
this.setData({ page: 1, recipeList: [] })
this.fetchRecipeList()
},
onReachBottom() {
// 触底加载更多
if (this.data.hasMore) {
this.setData({ page: this.data.page + 1 })
this.fetchRecipeList()
}
},
fetchRecipeList() {
const { page } = this.data
wx.request({
url: `https://api.example.com/recipes?page=${page}&size=10`,
success: (res) => {
const newList = this.data.recipeList.concat(res.data.items)
this.setData({
recipeList: newList,
hasMore: res.data.hasMore
})
}
})
}
})
2. components:复用组件的仓库
对于菜谱卡片、食材列表、步骤时间线这类在多个页面重复出现的 UI,没有重复编写代码,而是封装成了独立的自定义组件。
recipy-card:厨房首页和广场页都要用到的菜谱卡片。
ingredient-list:菜谱详情页和教学页都要展示的食材清单。
step-timeline:教学页的步骤指示器,支持自定义进度和状态。
优势:实现了"一次编写,多处使用",既减少了冗余代码,也保证了全项目 UI 风格的统一。
示例代码:自定义组件封装
javascript
// components/recipy-card/recipy-card.js
Component({
properties: {
recipeData: {
type: Object,
value: {},
observer: function(newVal) {
// 当传入数据变化时进行处理
this.processData(newVal)
}
},
showAuthor: {
type: Boolean,
value: true
}
},
data: {
displayTitle: '',
difficultyStars: [],
formattedTime: ''
},
methods: {
processData(data) {
// 处理难度星级显示
const stars = Array.from({ length: 5 }, (_, i) => i < data.difficulty)
this.setData({
displayTitle: data.title.length > 20
? data.title.substring(0, 20) + '...'
: data.title,
difficultyStars: stars,
formattedTime: `${data.cookingTime}分钟`
})
},
onCardTap() {
// 触发自定义事件,向父组件传递菜谱ID
this.triggerEvent('cardtap', {
recipeId: this.properties.recipeData.id
})
}
}
})
配套的组件模板文件:
xml
<!-- components/recipy-card/recipy-card.wxml -->
<view class="recipe-card" bindtap="onCardTap">
<image class="cover" src="{{recipeData.coverImage}}" mode="aspectFill"/>
<view class="info">
<text class="title">{{displayTitle}}</text>
<view class="meta">
<view class="difficulty">
<text wx:for="{{difficultyStars}}" wx:key="index">
{{item ? '★' : '☆'}}
</text>
</view>
<text class="time">{{formattedTime}}</text>
</view>
<view wx:if="{{showAuthor}}" class="author">
<text>{{recipeData.author}}</text>
</view>
</view>
</view>
3. utils:工具与通用逻辑的抽离
这里存放的是和业务无关、但全项目通用的工具函数:
request.js:封装了微信的 wx.request,统一处理请求头、响应拦截和错误提示,避免每个页面都写重复的请求逻辑。
auth.js:管理用户的登录状态,提供登录校验、获取用户信息等方法。
animation-map.js:核心模块,将后端 JSON 中返回的 animation 字段(如 cut、stir)映射到对应的 GIF 动画路径,实现了教学页动画的动态切换。
优势:通用逻辑集中管理,修改一处,全项目生效,极大提升了开发和维护效率。
示例代码:请求封装与拦截器
javascript
// utils/request.js
const BASE_URL = 'https://api.example.com'
class RequestManager {
constructor() {
this.pendingRequests = new Map()
this.maxRetries = 3
}
// 统一请求方法
request(options) {
const { url, method = 'GET', data = {}, needAuth = true } = options
return new Promise((resolve, reject) => {
// 请求拦截:添加token
if (needAuth) {
const token = wx.getStorageSync('token')
if (token) {
data.token = token
}
}
// 请求去重
const requestKey = `${method}_${url}_${JSON.stringify(data)}`
if (this.pendingRequests.has(requestKey)) {
return this.pendingRequests.get(requestKey)
}
const requestPromise = this.doRequest({
url: BASE_URL + url,
method,
data
})
this.pendingRequests.set(requestKey, requestPromise)
requestPromise.finally(() => {
this.pendingRequests.delete(requestKey)
})
return requestPromise
})
}
// 执行实际请求,包含重试机制
doRequest(config, retryCount = 0) {
return new Promise((resolve, reject) => {
wx.request({
...config,
success: (res) => {
// 响应拦截:统一错误处理
if (res.statusCode === 200) {
if (res.data.code === 0) {
resolve(res.data.data)
} else {
wx.showToast({
title: res.data.message || '请求失败',
icon: 'none'
})
reject(res.data)
}
} else if (res.statusCode === 401) {
// token过期,跳转登录
wx.redirectTo({ url: '/pages/login/login' })
reject(res)
} else {
reject(res)
}
},
fail: (err) => {
// 网络错误重试
if (retryCount < this.maxRetries) {
console.log(`请求失败,第${retryCount + 1}次重试`)
this.doRequest(config, retryCount + 1)
.then(resolve)
.catch(reject)
} else {
wx.showToast({
title: '网络连接失败',
icon: 'none'
})
reject(err)
}
}
})
})
}
get(url, data) {
return this.request({ url, method: 'GET', data })
}
post(url, data) {
return this.request({ url, method: 'POST', data })
}
}
module.exports = new RequestManager()
4. images:静态资源的统一管理
所有图片资源按用途分类存放,tab 放底部导航图标,gif 放步骤动画,cook-tab 放发布页图标。
优势:资源路径清晰,方便查找和替换,也为后续图片压缩和优化提供了便利。
示例代码:动画资源映射表
javascript
// utils/animation-map.js
const AnimationMap = {
// 基础处理动作
'cut': '/images/gif/cut.gif', // 切菜
'chop': '/images/gif/chop.gif', // 剁碎
'slice': '/images/gif/slice.gif', // 切片
'dice': '/images/gif/dice.gif', // 切丁
// 烹饪动作
'stir-fry': '/images/gif/stir-fry.gif', // 翻炒
'deep-fry': '/images/gif/deep-fry.gif', // 油炸
'steam': '/images/gif/steam.gif', // 蒸
'boil': '/images/gif/boil.gif', // 煮
'stew': '/images/gif/stew.gif', // 炖
// 调味动作
'season': '/images/gif/season.gif', // 调味
'marinate': '/images/gif/marinate.gif', // 腌制
'mix': '/images/gif/mix.gif', // 搅拌
// 默认动画
'default': '/images/gif/default-cooking.gif'
}
// 获取动画路径的方法
function getAnimationUrl(actionType) {
return AnimationMap[actionType] || AnimationMap.default
}
// 获取动画持续时间(配合动画节奏)
function getAnimationDuration(actionType) {
const DurationMap = {
'cut': 3000,
'chop': 2500,
'stir-fry': 4000,
'steam': 5000
}
return DurationMap[actionType] || 3000
}
module.exports = {
AnimationMap,
getAnimationUrl,
getAnimationDuration
}
三、核心配置文件解析
目录搭建完成后,需要重点关注两个关键配置文件。
1. app.json:小程序的全局蓝图
这个文件定义了小程序的页面路径、全局样式、底部导航栏等核心配置。
示例代码:全局配置实现
json
{
"pages": [
"pages/square/square",
"pages/login/login",
"pages/kitchen/kitchen",
"pages/mine/mine",
"pages/recipy-detail/recipy-detail",
"pages/recipy-edit/recipy-edit",
"pages/cook/cook",
"pages/teach/teach"
],
"tabBar": {
"color": "#999",
"selectedColor": "#333",
"list": [
{
"pagePath": "pages/kitchen/kitchen",
"text": "厨房",
"iconPath": "images/tab/kitchen.png",
"selectedIconPath": "images/tab/kitchen-active.png"
},
{
"pagePath": "pages/square/square",
"text": "广场",
"iconPath": "images/tab/square.png",
"selectedIconPath": "images/tab/square-active.png"
},
{
"pagePath": "pages/mine/mine",
"text": "我的",
"iconPath": "images/tab/mine.png",
"selectedIconPath": "images/tab/mine-active.png"
}
]
},
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "菜谱",
"navigationBarTextStyle": "black"
},
"style": "v2",
"sitemapLocation": "sitemap.json",
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于推荐附近菜系"
}
}
}
pages 数组:必须按顺序注册所有页面,第一个路径就是小程序启动的首页。
tabBar:配置了底部的"厨房 / 广场 / 我的"导航栏,指定了图标和文字。
window:设置了全局导航栏的背景色、标题文字和样式。
permission:提前声明需要的权限,避免运行时弹窗影响体验。
2. app.js:小程序的入口脚本
javascript
// app.js
App({
globalData: {
userInfo: null,
isLogin: false,
systemInfo: null
},
onLaunch() {
// 获取系统信息
this.globalData.systemInfo = wx.getSystemInfoSync()
// 初始化云开发环境
if (!wx.cloud) {
console.error('请使用 2.2.3 或以上的基础库以使用云能力')
} else {
wx.cloud.init({
env: 'cloud1-d3g0kjznk9caea1e8',
traceUser: true
})
}
// 检查登录状态
this.checkLoginStatus()
},
checkLoginStatus() {
const token = wx.getStorageSync('token')
if (token) {
this.globalData.isLogin = true
this.fetchUserInfo()
}
},
fetchUserInfo() {
wx.request({
url: 'https://api.example.com/user/info',
success: (res) => {
this.globalData.userInfo = res.data
// 触发全局事件
this.emit('userInfoUpdated', res.data)
}
})
},
// 简易事件系统
events: {},
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
},
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data))
}
}
})
这里主要做了小程序启动时的初始化工作,包括系统信息获取、云开发环境初始化、登录状态检查,并实现了一个简易的全局事件系统,为后续的跨页面通信提供支持。
四、今日总结与后续预告
Day1 完成了整个项目的初始化工作,搭建好了一个模块化、工程化的骨架。一个清晰的目录结构,是项目健康成长的第一步,它能让后续开发事半功倍。
架构设计的核心要点回顾:
业务分离:pages 目录按功能模块拆分,每个页面职责单一。
组件复用:components 目录统一管理可复用组件,减少代码冗余。
逻辑抽离:utils 目录集中处理通用逻辑,提升代码可维护性。
资源规整:images 目录按用途分类存储,便于资源管理。
配置分离:app.json 管理全局配置,app.js 处理初始化逻辑。
想要解锁更多微信小程序模块化实战、前后端联调排错、项目工程化规范干货、新手开发避坑指南吗?
持续关注,后续将更新组件封装、AI菜谱解析、步骤动画联动、后端接口开发等硬核内容,带你从新手快速进阶,轻松搞定小程序全栈开发!
想要解锁更多烹饪小程序架构搭建、前后端数据对接、数据库设计、接口日志调试实战干货、开发易错点避坑总结吗?
持续关注,后续将更新自定义步骤指示器、语音播报适配、菜谱上传发布、个人中心模块开发等硬核内容,带你从零搭建商用级菜谱小程序!