面向 REST API 构建前端:Flask + Connexion + 原生 JavaScript 实战指南(build-a-rest-api-frontend)
面向 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 文件。