面向 REST API 构建前端:Flask + Connexion + 原生 JavaScript 实战指南(build-a-rest-api-frontend)

原创2026-10-14 12:49:511,682 阅读
文章标签:示例工程

面向 REST API 构建前端:Flask + Connexion + 原生 JavaScript 实战指南(build-a-rest-api-frontend)

本指南以仓库 build-a-rest-api-frontend/README.md 为核心骨架,结合 source_code_start/ 到 source_code_final/ 的完整源码,系统讲解如何为 Connexion + Flask 构建的 REST API 编写一套不依赖任何前端框架的交互式界面。读完本文,你将掌握虚拟环境搭建、SQLite 数据库初始化、OpenAPI 服务启动,以及用原生 JavaScript(ES Modules)封装 XMLHttpRequest 实现人员与笔记的增删改查(CRUD)前端全流程。

一、项目定位与技术栈概览

build-a-rest-api-frontend 是 Real Python 教程《Build a Front End for a REST API》的配套代码仓库,其核心目标非常明确:为已有的 Flask REST API 补齐前端界面。仓库根目录的 README.md 给出了最简上手路径,而完整实现则分布在从 source_code_start(起始骨架)到 source_code_final(最终成品)的多级源码目录中,非常适合对照学习前后端如何一步步打通。

从源码结构看,该项目采用"后端 API + 服务端渲染模板 + 原生 JS 增强"的混合架构:

层次 技术选型 仓库证据
API 框架 Connexion 2.14.1(基于 OpenAPI 3.0 规范) requirements.txt
Web 框架 Flask 2.2.2 requirements.txt
ORM Flask-SQLAlchemy 3.0.2 + SQLAlchemy 1.4.42 requirements.txt
序列化 Flask-Marshmallow 0.14.0 requirements.txt
数据库 SQLite(people.db) config.py
前端 原生 JavaScript ES Modules + Jinja2 模板 static/js/index.js

该项目的设计思路值得关注:API 层由 swagger.yml 单一文件驱动(Connexion 根据规范自动路由),而前端层则完全使用原生 JavaScript 的模块化写法,不引入 jQuery、React 或 Vue,让读者能清晰看到 fetch/XMLHttpRequest 层面最本质的请求封装方式。

二、环境搭建:虚拟环境与依赖安装

1. 创建虚拟环境

README 中的第一步是创建并激活 Python 虚拟环境,将项目依赖与系统环境隔离:

$ python -m venv venv
$ source venv/bin/activate
  • python -m venv venv 会在当前目录生成 venv/ 目录,其中包含独立的 Python 解释器与 pip。
  • source venv/bin/activate 仅适用于 Linux / macOS 的 bash 或 zsh 终端;Windows PowerShell 用户需改用 venv\Scripts\Activate.ps1。
  • 激活后命令行提示符会出现 (venv) 前缀,此后 pip 与 python 均指向虚拟环境。

2. 安装锁定依赖

(venv) $ python -m pip install -r requirements.txt

requirements.txt 中所有依赖均以精确版本锁定(如 connexion==2.14.1、Flask==2.2.2、SQLAlchemy==1.4.42),这保证了不同机器上行为一致。值得注意的几点:

  • connexion 是整套 API 的核心,它读取 swagger.yml 自动注册路由,并内置 /api/ui 的 Swagger UI 文档页面。
  • swagger-ui-bundle 为 Swagger UI 提供静态资源,没有它 /api/ui 无法渲染。
  • flask-marshmallow / marshmallow-sqlalchemy 负责将 ORM 模型对象与 JSON 之间互转,是 API 层序列化的关键。
  • jsonschema 被 Connexion 用于校验请求体是否符合 OpenAPI 定义。

三、初始化数据库:seed 数据与建表逻辑

1. 执行初始化脚本

依赖安装完成后,进入起始代码目录并运行初始化脚本:

(venv) $ cd source_code_start
(venv) $ python init_database.py

这一步会创建或更新 people.db 数据库文件,供后续 Flask 服务使用。

2. 初始化脚本源码拆解

init_database.py 的逻辑非常清晰,分为三部分:

(1)种子数据 PEOPLE_NOTES:内置了 3 位人物(Tooth Fairy、Knecht Ruprecht、Easter Bunny)及其带时间戳的笔记,例如:

PEOPLE_NOTES = [
    {
        "lname": "Fairy",
        "fname": "Tooth",
        "notes": [
            ("I brush my teeth after each meal.", "2022-01-06 17:10:24"),
            ("The other day a friend said, I have big teeth.", "2022-03-05 22:17:54"),
            ("Do you pay per gram?", "2022-03-05 22:18:10"),
        ],
    },
    # ...
]

(2)create_database():当数据库为空时调用。它执行 db.create_all() 依据模型建表,然后遍历 PEOPLE_NOTES,用 datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") 解析时间戳并创建 Person 及其关联 Note,最后提交事务:

def create_database(db):
    db.create_all()
    for data in PEOPLE_NOTES:
        new_person = Person(lname=data.get("lname"), fname=data.get("fname"))
        for content, timestamp in data.get("notes", []):
            new_person.notes.append(
                Note(content=content, timestamp=datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S"))
            )
        db.session.add(new_person)
    db.session.commit()
    print("Created new database")

(3)update_database():当数据库中已存在数据时调用,用于模型变更后的表结构重建。它先 db.drop_all() 再 db.create_all(),并通过 db.session.merge() 回填旧数据,避免重复建库时报错。

入口处用 app.app_context() 包裹查询逻辑,先探测 Person 表是否存在记录:

with app.app_context():
    existing_people = get_data_from_table(Person)
    existing_notes = get_data_from_table(Note)
    if not existing_people:
        create_database(db)
    else:
        update_database(db, existing_people, existing_notes)

其中 get_data_from_table() 捕获 OperationalError 并返回空列表——首次运行、表尚未创建时会走 create_database 分支;二次运行、表已存在且有数据时会走 update_database 分支,这正是 README 所说"创建或更新"的实现原理。

3. 数据模型设计

models.py 定义了 Person 与 Note 两个模型及其一对多关系:

  • Person:id(主键)、lname(唯一)、fname、timestamp(default=datetime.utcnow, onupdate=datetime.utcnow,插入与更新时自动记录时间)。
  • Note:id(主键)、person_id(外键关联 person.id)、content、timestamp。
  • 关系配置:Person.notes = db.relationship(Note, backref="person", cascade="all, delete, delete-orphan", order_by="desc(Note.timestamp)")——级联删除保证删除人员时其笔记一并清除,排序规则让笔记按时间倒序展示。

序列化方面,NoteSchema 与 PersonSchema 继承 ma.SQLAlchemyAutoSchema,通过 include_relationships = True 与 fields.Nested(NoteSchema, many=True) 将关联笔记嵌套进人员 JSON 中,并预生成三个实例:note_schema、person_schema、people_schema(多人列表)。

四、启动 Flask 服务并访问前端

1. 运行命令

(venv) $ python app.py

README 明确指出,服务启动后有两个访问入口:

  • 前端页面:http://localhost:8000
  • API 交互文档(Swagger UI):http://localhost:8000/api/ui

2. 应用入口源码拆解

app.py 全貌如下:

import config
from flask import render_template
from models import Person

app = config.connex_app
app.add_api(config.basedir / "swagger.yml")

@app.route("/")
def home():
    people = Person.query.all()
    return render_template("home.html", people=people)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, debug=True)

关键点解读:

  • app.add_api(config.basedir / "swagger.yml"):让 Connexion 读取 swagger.yml,依据其中 paths 与 operationId 自动把 /api/people、/api/notes 等请求路由到 people.py、notes.py 中的对应函数。
  • @app.route("/") 主页处理器从数据库查询全部人员,交给 home.html 模板渲染——这是前后端交汇的"桥头堡"。
  • app.run(host="0.0.0.0", port=8000, debug=True):绑定 8000 端口并开启调试模式。0.0.0.0 意味着局域网内其他机器也可访问;debug=True 支持代码热重载,但仅适合开发环境。

3. 应用配置:config.py

config.py 集中了 Connexion、SQLAlchemy 与 Marshmallow 的初始化:

basedir = pathlib.Path(__file__).parent.resolve()
connex_app = connexion.App(__name__, specification_dir=basedir)

app = connex_app.app
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{basedir / 'people.db'}"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

db = SQLAlchemy(app)
ma = Marshmallow(app)
  • specification_dir=basedir 告诉 Connexion 从 config.py 所在目录寻找 swagger.yml。
  • SQLALCHEMY_DATABASE_URI 指向项目目录下的 SQLite 文件 people.db(f"sqlite:///{basedir / 'people.db'}"),与 init_database.py 生成的库文件一一对应。
  • SQLALCHEMY_TRACK_MODIFICATIONS = False 关闭 SQLAlchemy 的事件追踪,减少不必要的内存开销。

五、API 层:OpenAPI 规范驱动的 CRUD 端点

1. swagger.yml 的端点全貌

swagger.yml 采用 OpenAPI 3.0.0 规范,servers 统一前缀为 /api,共暴露 6 组操作:

方法与路径 operationId 映射函数 说明
GET /api/people people.read_all people.py 的 read_all() 读取全部人员
POST /api/people people.create create() 新建人员(lname 必填)
GET /api/people/{lname} people.read_one read_one() 按姓氏读取单个人员
PUT /api/people/{lname} people.update update() 更新人员
DELETE /api/people/{lname} people.delete delete() 删除人员
POST /api/notes notes.create notes.py 的 create() 为指定 person_id 创建笔记
GET /api/notes/{note_id} notes.read_one read_one() 读取单条笔记
PUT /api/notes/{note_id} notes.update update() 更新笔记内容
DELETE /api/notes/{note_id} notes.delete delete() 删除笔记

components.schemas.Person 规定 lname 为必填字段;components.parameters 定义了路径参数 lname(string)与 note_id(integer)。请求体通过 x-body-name 指定传入后端函数的参数名(如 person、note),这是 Connexion 将 JSON body 绑定到 Python 函数参数的关键机制。

2. people.py 的业务实现

people.py 展示了典型的 CRUD 模式:

  • read_all():Person.query.all() 后经 people_schema.dump(people) 序列化为 JSON 列表。
  • create():先按 lname 查重(Person.query.filter(Person.lname == lname).one_or_none()),不存在才通过 person_schema.load(person, session=db.session) 反序列化、入库并返回 201;若已存在则 abort(406, ...) 返回 406 Not Acceptable。
  • read_one() / update() / delete():均以 lname 为查询键;不存在时 abort(404, ...)。更新采用 db.session.merge(existing_person) 合并改动;删除成功后返回 200。

3. notes.py 的关联写入

notes.py 中 create() 体现了外键关联的写入方式:

def create(note):
    person_id = note.get("person_id")
    person = Person.query.get(person_id)
    if person:
        new_note = note_schema.load(note, session=db.session)
        person.notes.append(new_note)
        db.session.commit()
        return note_schema.dump(new_note), 201
    else:
        abort(404, f"Person not found for ID: {person_id}")

即通过 person.notes.append() 把新笔记挂到既有人员对象上,由 ORM 自动填充外键——前端只需提交 person_id 与 content 两个字段即可。

六、前端实现:原生 JavaScript 模块化交互

1. 页面模板骨架

最终版 home.html 通过 {% include %} 将页面拆分为多个局部模板:_person_create_form.html(新建人员表单)、_person_content.html(人员卡片 + 笔记列表)、_debug.html(调试面板,仅 config['DEBUG'] 为真时渲染)。脚本以 ES Module 方式引入:

<script src="{{ url_for('static', filename='js/index.js') }}" type="module"></script>

type="module" 是整套前端能使用 import/export 语法的前提;浏览器原生加载模块后,页面无需任何打包工具即可运行。

_person_content.html 为每个人员卡片设置 data-person-id,并循环渲染其笔记;_note_create_form.html 中通过 <input name="person_id" type="hidden" value="{{ person.id }}" /> 把人员 ID 写入隐藏字段;_person_control.html 则承载编辑、取消、删除三个操作按钮(data-action="update|cancel|delete")。

2. 请求封装层:request.js

request.js 是整个前端的"网络层",只导出两个函数:

  • getData(endpoint, callback):发起 GET 请求,readyState === 4 时把 request.response 交给回调。
  • sendForm(form, action, endpoint, callback):用 FormData 收集表单、Object.fromEntries(formData) 转为对象、JSON.stringify 序列化,再以指定 method(POST/PUT/DELETE)发送,并设置 Content-Type: application/json。
export function sendForm(form, action, endpoint, callback) {
  const formData = new FormData(form);
  const dataJSON = JSON.stringify(Object.fromEntries(formData));
  const request = new XMLHttpRequest();
  request.onreadystatechange = () => {
    if (request.readyState === 4) {
      callback(request.response, form);
    }
  };
  request.open(action, endpoint);
  request.setRequestHeader("Content-Type", "application/json");
  request.send(dataJSON);
}

这套封装把"表单 → JSON → 请求 → 回调"的链路统一起来,后续的增删改操作全部复用 sendForm,是理解整个前端交互的核心。

3. 模块入口:index.js

index.js 是前端启动入口,main() 中实例化 People、Notes,若页面存在调试卡片则创建 DebugForm 并清空响应区。三个模块各司其职:

  • people.js:People 类激活新建表单并为每张人员卡片绑定控制逻辑;CreatePersonForm 在 POST 成功后用 cloneNode(true) 克隆现有卡片模板插入新人员,避免整页刷新;PersonControl 负责"编辑/取消/删除/更新"四个按钮的显隐切换与事件绑定,删除前用 window.confirm() 二次确认。
  • notes.js:NoteCreateForm 在构造时通过 connectPerson() 把 data-person-id 写入隐藏的 person_id 字段,POST 成功后按返回的 person_id 定位对应卡片、克隆 .note-card 并插入列表第二位。
  • debug.js:DebugForm 读取输入框中的 endpoint(默认 /api/people),调用 getData 并把原始 JSON 响应渲染到 <code> 中——这是不打开浏览器开发者工具也能直观查看 API 返回值的实用调试面板。

可以看到,前端所有更新操作都通过"克隆模板 + 局部插入"实现,这正是无框架 SPA 的经典做法:数据变化只更新 DOM 中受影响的部分,而不是整页刷新。

七、分步源码目录:一条清晰的演进路线

仓库按教程进度提供了多个快照目录,方便按阶段对照学习:

目录 阶段要点
source_code_start 纯服务端渲染:home.html 仅用 Jinja2 循环输出人员与笔记,无任何 JS/CSS
source_code_step_1 与 start 相同的基础骨架
source_code_step_2 引入全局样式 style.css 与调试面板 _debug.html
source_code_step_3 加入 request.js、debug.js、index.js,前端模块化起步,可在调试面板直接调用 API
source_code_step_4 新增 people.css 与 people.js,实现人员的创建与增删改交互
source_code_step_5 补齐 notes.css 与 notes.js,实现笔记创建,功能闭环
source_code_final 完整最终版,五步成果合体

对比起始版 home.html(仅 h2 + ul 的无样式列表)与最终版 home.html,可以清晰看到"纯服务端渲染 → 静态资源引入 → 模块化 JS 交互 → 局部更新"的完整演变,这是理解前后端渐进增强模式的最佳教材。

八、运行验证与常见问题

1. 用 curl 快速验证 API

服务启动后,可在另一终端直接验证后端接口:

# 读取全部人员
$ curl http://localhost:8000/api/people

# 创建人员(lname 必填)
$ curl -X POST http://localhost:8000/api/people \
       -H "Content-Type: application/json" \
       -d '{"fname": "Test", "lname": "Case"}'

# 为 ID 为 1 的人员创建笔记
$ curl -X POST http://localhost:8000/api/notes \
       -H "Content-Type: application/json" \
       -d '{"person_id": 1, "content": "Hello API"}'

2. 常见问题排查

  • people.db 未生成或结构过期:重新运行 python init_database.py,脚本会根据表是否存在自动走 create_database 或 update_database 分支。
  • /api/ui 打不开:确认已安装 swagger-ui-bundle==0.0.9(包含在 requirements.txt 中)。
  • 前端 JS 不生效:确认浏览器支持 ES Modules,且页面以 http://localhost:8000 提供服务(直接双击打开本地 HTML 文件会因模块跨域限制而失败)。
  • 端口被占用:若 8000 被占用,修改 app.py 中的 port 参数即可。

九、总结

通过本仓库可以完整掌握一条现代化的"轻前端"实战路径:先由 swagger.yml 定义全部 API 契约,Connexion 自动路由到 people.py 与 notes.py;再用 Flask 模板渲染首屏数据,最后用原生 JavaScript 的 XMLHttpRequest 封装(request.js)驱动人员与笔记的无刷新增删改。整个过程不依赖任何前端框架与构建工具,对理解 HTTP 协议、OpenAPI 规范、ORM 关联写入以及 DOM 局部更新原理都有直接帮助,是一份可运行、可逐级对照的开源学习材料。

项目源码基于 MIT 许可证分发,详见仓库根目录的 LICENSE 文件。

登录后查看全文
materials