模块化烹饪小程序开发日记 Day4:网络层基础设施与接口治理实践

前言

在烹饪小程序的开发进程中,前后端联调是连接前端界面与后端数据的核心环节。本篇基于微信小程序与 Python Flask 技术栈,完成从请求工具封装、拦截器实现、错误统一处理到实际接口联调的全流程实战。通过封装通用请求模块,解决原生请求的代码冗余与维护困难问题,同时梳理联调过程中的高频问题与解决方案,为小程序的功能落地奠定坚实基础。

一、技术栈与开发环境说明

在正式进入封装工作之前,需要先明确本阶段所采用的技术架构。前后端分离是目前主流的开发模式,前端专注界面交互与用户体验,后端专注数据处理与业务逻辑。

核心技术选型如下:前端采用微信小程序原生开发,不引入第三方框架,保持代码轻量与学习曲线的平缓。后端采用 Python Flask 框架,配合 Flask-SQLAlchemy 操作 MySQL 数据库。前后端之间通过 HTTP 协议进行通信,数据交互格式以 JSON 为主,文件上传场景则使用 form-data 格式。

后端基础配置已经完成,包括数据库连接初始化、跨域中间件配置、文件上传目录设置、菜谱 CRUD 接口以及 AI 菜谱解析等核心功能。这些准备工作确保了前端可以正常发起请求并获得响应。

python 复制代码
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from datetime import datetime

app = Flask(__name__)
CORS(app)

# 数据库配置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:password@localhost/cooking'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

# 文件上传配置
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024

以上代码展示了后端 Flask 应用的基础配置骨架。CORS(app) 这一行尤为关键,它启用了跨域资源共享,允许来自小程序前端的请求顺利抵达后端服务,是前后端联调成功的第一道关口。

二、原生请求方式的痛点分析

微信小程序提供了 wx.request API 作为网络请求的基础能力。然而在实际项目开发中,直接使用原生 API 会逐渐暴露出诸多问题。理解这些痛点,是驱动我们进行封装的根本动机。

第一个痛点是代码冗余。每一个接口调用都需要重复编写完整的请求地址、请求方法、请求头、超时时间以及错误处理回调。当接口数量增长到十几个甚至几十个时,这些重复代码会让项目变得臃肿不堪。

第二个痛点是维护困难。假设后端服务的域名发生变更,或者某个请求头的格式需要调整,开发者不得不在散落于各个页面的请求代码中逐一修改,遗漏的风险极高。

第三个痛点是错误处理不统一。有的页面可能只处理了网络错误而忽略了业务异常,有的页面可能完全没有错误提示,导致用户体验割裂且难以追踪问题。

第四个痛点是缺少统一拦截能力。无法在请求发起前全局添加认证 Token,也无法在收到响应后统一解析数据格式。

javascript 复制代码
// 原生写法示例:每个接口都需要完整配置
wx.request({
  url: 'http://127.0.0.1:5000/api/food/list',
  method: 'GET',
  header: { 'Content-Type': 'application/json' },
  timeout: 10000,
  success: (res) => {
    if (res.statusCode === 200 && res.data.code === 200) {
      console.log(res.data.data);
    } else {
      wx.showToast({ title: '请求失败', icon: 'none' });
    }
  },
  fail: () => {
    wx.showToast({ title: '网络异常', icon: 'none' });
  }
});

这段原生代码暴露了上述所有问题。如果我们有二十个接口,就意味着二十次类似的重复配置,这正是封装要解决的核心矛盾。

三、请求工具类的整体架构设计

基于上述痛点分析,我们确定了封装目标:创建一个全局通用的请求工具模块,统一管理基础配置,提供请求与响应拦截机制,实现错误的集中处理,并将接口调用按业务模块进行组织。

封装后的调用方式应当简洁如一行代码,让页面开发者无需关心底层网络细节。

javascript 复制代码
// 封装后期望的调用方式
import { getFoodList } from '../../api/food';

const res = await getFoodList();

这种调用方式的优雅之处在于,它隐藏了 URL 拼接、请求头设置、错误弹窗、加载提示等所有与业务无关的技术细节。页面开发者只需关注数据本身,这正是良好封装带来的价值。

四、基础配置与请求类初始化

封装的第一步是创建一个请求类,将基础配置集中管理。基础 URL、超时时间等全局参数作为类的属性存在,方便统一修改与维护。

javascript 复制代码
const baseUrl = "http://127.0.0.1:5000/api";
const timeout = 10000;

class Request {
  constructor() {
    this.baseUrl = baseUrl;
    this.timeout = timeout;
  }
}

export default new Request();

这里采用了单例模式导出实例,确保整个小程序运行期间只有一个请求工具实例,所有接口调用共享同一套配置。将 baseUrl 定义为常量,可以在部署环境切换时仅修改一处即可生效。

五、请求拦截器的实现逻辑

请求拦截器是在请求发出之前执行的函数,用于统一处理请求配置。我们在这里完成三件事:拼接完整的请求 URL、设置超时时间与默认请求头、显示加载提示。

javascript 复制代码
requestInterceptors(options) {
  options.url = this.baseUrl + options.url;
  options.timeout = this.timeout;
  options.header = {
    "Content-Type": "application/json",
    ...options.header
  };
  wx.showLoading({
    title: "加载中...",
    mask: true
  });
  return options;
}

URL 拼接的设计让接口模块中只需写相对路径,如 /food/list,拦截器会自动补全为 http://127.0.0.1:5000/api/food/list。请求头的合并使用了展开运算符,既设定了 JSON 的默认格式,又允许调用方传入自定义头部进行覆盖。加载提示的 mask: true 参数会阻止用户在请求期间进行其他操作,防止重复提交。

六、响应拦截器与业务状态码处理

响应拦截器在收到服务器返回后执行,负责关闭加载提示、判断 HTTP 状态码与业务状态码,将后端返回的标准化数据解析后传递给调用方。

javascript 复制代码
responseInterceptors(response) {
  wx.hideLoading();
  const { statusCode, data } = response;
  
  if (statusCode === 200) {
    if (data.code === 200 || data.success === true) {
      return data;
    } else {
      wx.showToast({
        title: data.msg || "请求失败",
        icon: "none"
      });
      return Promise.reject(data);
    }
  } else {
    this.handleHttpError(statusCode);
    return Promise.reject(response);
  }
}

这段逻辑建立了一个双层判断机制。外层判断 HTTP 状态码,200 表示网络通信成功,其他状态码则进入错误处理分支。内层判断业务状态码,即使 HTTP 层面成功,后端仍可能返回业务异常,例如参数校验失败。通过 Promise.reject 将错误向下传递,让调用方可以通过 try-catch 捕获并处理。

七、HTTP 错误状态码的统一映射

对于不同的 HTTP 错误状态码,我们建立一套映射关系,将其转换为用户可读的中文提示信息。这不仅提升了用户体验,也让开发者无需在每个页面重复编写错误提示逻辑。

javascript 复制代码
handleHttpError(statusCode) {
  let errMsg = "";
  switch (statusCode) {
    case 400: errMsg = "请求参数错误"; break;
    case 401: errMsg = "未授权,请重新登录"; break;
    case 404: errMsg = "请求资源不存在"; break;
    case 500: errMsg = "服务器内部错误"; break;
    default: errMsg = "网络请求失败";
  }
  wx.showToast({ title: errMsg, icon: "none" });
}

400 错误通常意味着前端提交的参数不符合后端预期,开发者需要检查参数格式。404 表示请求的接口路径不存在,可能是 URL 拼接错误。500 则是后端代码运行异常,需要查看服务端日志。这套分类处理机制让问题的定位更加高效。

八、核心请求方法的 Promise 封装

微信小程序的 wx.request 本身基于回调函数设计,为了支持现代化的 async/await 语法,我们需要将其包装为 Promise 对象。同时,在这个方法中整合请求拦截器和响应拦截器。

javascript 复制代码
request(options) {
  options = this.requestInterceptors(options);
  return new Promise((resolve, reject) => {
    wx.request({
      ...options,
      success: (res) => {
        resolve(this.responseInterceptors(res));
      },
      fail: (err) => {
        wx.hideLoading();
        wx.showToast({ title: "网络异常,请检查连接", icon: "none" });
        reject(err);
      }
    });
  });
}

get(url, data = {}, options = {}) {
  return this.request({ url, method: "GET", data, ...options });
}

post(url, data = {}, options = {}) {
  return this.request({ url, method: "POST", data, ...options });
}

request 方法是整个工具的核心。它首先执行请求拦截器修改配置,然后返回一个 Promise,在 success 回调中执行响应拦截器并 resolve,在 fail 回调中弹出网络异常提示并 reject。get 和 post 方法则是对 request 的便捷封装,让接口调用时无需每次指定请求方法。

九、接口模块化管理方案

随着项目规模增长,所有接口定义堆砌在一个文件中会导致难以维护。按业务领域拆分接口文件是成熟的工程实践。本项目划分为菜谱模块、文件上传模块和 AI 解析模块。

javascript 复制代码
import request from "../utils/request";

export const getFoodList = () => {
  return request.get("/food/list");
};

export const getFoodDetail = (foodId) => {
  return request.get(`/food/${foodId}`);
};

export const addFood = (data) => {
  return request.post("/food/add", data);
};

export const parseRecipe = (content) => {
  return request.post("/parse_recipe", { content });
};

注意 getFoodDetail 接口使用了 ES6 模板字符串动态拼接路径参数,这是一种常见的 RESTful 风格接口调用方式。每个接口函数都返回一个 Promise,使得页面调用时可以直接使用 await 等待结果。

十、文件上传的特殊处理

文件上传与普通 JSON 请求有显著区别。微信小程序提供了专用的 wx.uploadFile API,它使用 form-data 格式而非 JSON,且上传进度回调机制也与普通请求不同。因此需要单独封装。

javascript 复制代码
export const uploadImage = (filePath) => {
  return new Promise((resolve, reject) => {
    wx.uploadFile({
      url: request.baseUrl + "/upload",
      filePath: filePath,
      name: "image",
      header: { "Content-Type": "form-data" },
      success: (res) => {
        const data = JSON.parse(res.data);
        if (data.code === 200) {
          resolve(data);
        } else {
          wx.showToast({ title: data.msg, icon: "none" });
          reject(data);
        }
      },
      fail: reject
    });
  });
};

注意这里手动拼接了完整 URL,因为 wx.uploadFile 不经过 requestInterceptors。另外,wx.uploadFile 返回的 res.data 是字符串类型,需要手动调用 JSON.parse 解析。文件上传的字段名 name: "image" 必须与后端接收文件的字段名严格一致。

十一、页面调用接口的实战模式

完成工具封装与接口定义后,页面中的调用代码变得极其简洁。通过 async/await 语法,开发者可以像写同步代码一样处理异步请求。

javascript 复制代码
import { getFoodList } from "../../api/food";

Page({
  data: { foodList: [] },

  onLoad() {
    this.getFoodListData();
  },

  async getFoodListData() {
    try {
      const res = await getFoodList();
      this.setData({ foodList: res.data });
    } catch (error) {
      console.log("获取列表失败", error);
    }
  }
});

try-catch 块捕获的不仅是网络异常,也包括响应拦截器中 reject 的业务异常。由于拦截器已经通过 wx.showToast 向用户展示了错误信息,页面中只需记录日志或执行额外的状态重置即可,无需再次弹窗。

十二、前后端联调高频问题与解决方案

跨域问题是小程序本地联调中最常见的拦路虎。小程序开发者工具发起的请求与浏览器类似,同样受到同源策略的限制。解决方案是后端安装 flask-cors 中间件并全局启用,同时在小程序开发者工具中开启域名校验跳过选项。

python 复制代码
from flask_cors import CORS
CORS(app)

请求格式不匹配是另一个高频问题。纯 JSON 数据请求需要设置 Content-Type: application/json,后端通过 request.get_json() 获取。文件上传则必须使用 wx.uploadFile,后端通过 request.files 获取文件对象。

本地服务无法访问通常出现在真机调试场景。小程序运行在手机上时,127.0.0.1 指向的是手机本身而非开发电脑。解决方案是将 Flask 绑定到 0.0.0.0,允许局域网内所有设备访问,然后将前端的 baseUrl 改为电脑的局域网 IP 地址。

python 复制代码
app.run(host='0.0.0.0', port=5000, debug=True)

十三、后端接口标准化与数据模型

为了保证前后端协作的高效,后端接口需要遵循统一的返回格式。本项目定义了标准化响应结构,成功时返回 code: 200 与 data 字段,失败时返回相应的错误码与 msg 描述。

python 复制代码
class Food(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(200), nullable=False)
    image_url = db.Column(db.String(500))
    desc = db.Column(db.Text)
    structured_data = db.Column(db.Text)
    create_time = db.Column(db.DateTime, default=datetime.now)

这个数据模型涵盖了菜谱的核心字段。structured_data 字段以文本形式存储 AI 解析出的结构化菜谱数据,create_time 使用默认值自动记录创建时间。这些字段的设计直接决定了前端页面的展示能力。

总结

本篇完成了烹饪小程序前后端联调的核心工作,通过封装 request.js 请求工具,实现了请求拦截、响应拦截、统一错误处理与接口模块化管理,大幅提升了代码的可维护性与开发效率。同时结合实际开发场景,梳理了跨域、请求格式、网络访问等高频问题的解决方案。通过本次封装,小程序前端所有网络请求均可通过简洁的 API 调用实现,无需关注底层网络逻辑;后端提供标准化接口,保证了前后端数据交互的稳定性,为后续小程序的功能开发、界面完善与项目上线提供了坚实的网络层支撑。


想要解锁更多小程序组件化封装、JSON 结构化菜谱解析、Lottie/GIF 动画适配、全栈项目落地实战干货、零基础入门避坑教程吗?
持续关注,后续将更新云端部署、跨端适配、样式统一美化、历史菜谱收藏功能等硬核内容,手把手带你吃透小程序全栈开发流程!

相关推荐
冴羽yayujs4 小时前
快速夯实 JavaScrilpt 基础的 33 个概念
前端·javascript·github·前端开发
放下华子我只抽RuiKe54 小时前
React 从入门到生产(二):状态与事件处理
前端·人工智能·深度学习·react.js·机器学习·前端框架·github
鹏大师运维4 小时前
信创数据库开发--SQLark这款工具支持麒麟、统信
linux·数据库·数据库开发·麒麟·统信·sqlark·桌面操作系统
Maimai108084 小时前
React 项目目录结构怎么设计:从基础分层到真实业务落地
前端·javascript·react.js·microsoft·前端框架
开开心心就好4 小时前
带OCR识别的电子发票打印工具
运维·javascript·科技·游戏·青少年编程·ocr·powerpoint
Csvn4 小时前
CSS 技巧:移动端适配
前端
林夕074 小时前
Qt QML与C++混合编程实战指南
java·开发语言·数据库
hyunbar4 小时前
高级 SQL 实战教程(华为云 DWS / PostgreSQL 版)
linux·服务器·数据库
phltxy4 小时前
Redis 缓存
数据库·redis·缓存