前言
在烹饪小程序的开发进程中,前后端联调是连接前端界面与后端数据的核心环节。本篇基于微信小程序与 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 动画适配、全栈项目落地实战干货、零基础入门避坑教程吗?
持续关注,后续将更新云端部署、跨端适配、样式统一美化、历史菜谱收藏功能等硬核内容,手把手带你吃透小程序全栈开发流程!