今天,正式开启一个全新的技术系列 —— 从零构建一个带 AI 解析、步骤动画、教学演示的全栈烹饪教程微信小程序。

这个项目是一次技术栈的综合实践,也是对工程化开发理念的深度探索。本系列将完整记录从 0 到 1 的构建过程,包括架构设计、前后端联调、AI 功能接入和用户体验优化,希望能为同样在学习路上的开发者带来一些启发。
在这里插入图片描述

一、项目定位与开发目标

在动手编码之前,首先需要明确项目的核心定位:

一个集菜谱发布、AI 解析、分步教学、动画演示于一体的社区化烹饪小程序。

基于这个定位,设定了几个核心目标:

结构化菜谱教学:通过 JSON 结构化数据,实现可动态配置的分步教学流程。

沉浸式动画体验:为不同烹饪步骤(切菜、翻炒、蒸煮)匹配专属 GIF 动画,提升教学直观性。

AI 赋能内容生成:用户上传菜谱文案,后端 AI 自动解析为标准化 JSON 数据,降低发布门槛。

工程化模块化架构:采用清晰的目录结构和组件化设计,保证项目可维护、可扩展。

为了实现这些目标,第一步,也是最关键的一步,就是搭建好项目的骨架。

示例代码:菜谱结构化数据模型

// 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 文件夹,导致后期维护起来像一团乱麻。这次采用了一套高内聚、低耦合的模块化目录设计,结构如下:

├── 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 文件。

优势:业务边界清晰,修改某个页面不会影响其他模块,也方便后续按需分包加载,优化启动速度。

示例代码:页面注册与生命周期
// 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 风格的统一。

示例代码:自定义组件封装
// 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 
      })
    }
  }
})

配套的组件模板文件:

<!-- 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 动画路径,实现了教学页动画的动态切换。

优势:通用逻辑集中管理,修改一处,全项目生效,极大提升了开发和维护效率。

示例代码:请求封装与拦截器
// 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 放发布页图标。

优势:资源路径清晰,方便查找和替换,也为后续图片压缩和优化提供了便利。

示例代码:动画资源映射表
// 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:小程序的全局蓝图

这个文件定义了小程序的页面路径、全局样式、底部导航栏等核心配置。

示例代码:全局配置实现
{
  "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:小程序的入口脚本

// 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菜谱解析、步骤动画联动、后端接口开发等硬核内容,带你从新手快速进阶,轻松搞定小程序全栈开发!


想要解锁更多烹饪小程序架构搭建、前后端数据对接、数据库设计、接口日志调试实战干货、开发易错点避坑总结吗?

持续关注,后续将更新自定义步骤指示器、语音播报适配、菜谱上传发布、个人中心模块开发等硬核内容,带你从零搭建商用级菜谱小程序!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐