1 前言
其实一开始我是会这个知识点的,只是太久没用过了。大二上之后就没怎么正经写过代码了,所以已经忘得差不多了,刚好趁着写博客把这些东西都捡起来。
2 整体链路
这一篇文章先只写前端到 Router 里面的方法匹配,剩下的部分放到后两篇文章里面再说。
整体链路如下:
按下前端的按钮
↓
监听器监听到 submit 事件
↓
触发 fetch,fetch 发送 HTTP 请求到后端
↓
FastAPI 根据请求方法和路由匹配 Router 里的处理函数
3 前端的按钮
这个网站是一个非常简单的、用来帮助理解的小工程,我之后会把这个压缩包放在可以下载的地方。
先看一下这个网页:

接下来看到前端对应的网页,也就是:
static/index.html
这里面和这一篇有关的关键代码主要是下面这些。
<button type="submit">创建文章</button>
const form = document.querySelector('#article-form');
const requestPreview = document.querySelector('#request-preview');
const responsePreview = document.querySelector('#response-preview');
const articlesPreview = document.querySelector('#articles');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const body = {
title: document.querySelector('#title').value,
content: document.querySelector('#content').value,
};
requestPreview.textContent =
`POST /api/articles\n\n${JSON.stringify(body, null, 2)}`;
responsePreview.textContent = '请求中...';
const response = await fetch('/api/articles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await response.json();
responsePreview.textContent =
`${response.status} ${response.statusText}\n\n${JSON.stringify(data, null, 2)}`;
});
document.querySelector('#reload').addEventListener('click', async () => {
const response = await fetch('/api/articles');
const data = await response.json();
articlesPreview.textContent = JSON.stringify(data, null, 2);
});
3.1 submit 是什么
把注意力放到这一段代码上:
<button type="submit">创建文章</button>
type="submit" 表示这个按钮是表单的“提交按钮”。
点击它时,会触发所在 <form> 的 submit 事件。
3.2 事件监听器
再看这一段:
form.addEventListener('submit', async (event) => {
event.preventDefault();
const body = {
title: document.querySelector('#title').value,
content: document.querySelector('#content').value,
};
第一行的意思是监听 submit 这个事件。
如果表单触发了 submit,就执行后面的这个函数。
3.2.1 event.preventDefault()
这一句话比较关键。
浏览器原本对表单 submit 有自己的默认提交行为,可能导致页面跳转或者刷新。
这里调用:
event.preventDefault();
就是阻止浏览器执行默认行为,然后改由我们自己的 JavaScript 使用 fetch() 来发送请求。
所以这里实际发生的是:
点击“创建文章”
↓
button type="submit"
↓
<form> 触发 submit
↓
JavaScript 监听到 submit
↓
preventDefault() 阻止默认提交
↓
继续执行我们自己写的逻辑
3.3 fetch 是什么
const response = await fetch('/api/articles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
这里的 fetch 就是真正发送 HTTP 请求的部分。
几个参数分别表示:
method: 'POST'
→ HTTP 请求方法
Content-Type: application/json
→ 告诉后端,请求正文使用 JSON
body: JSON.stringify(body)
→ 把 JavaScript 对象转换成 JSON 字符串,
放进 HTTP Request Body
3.3.1 body 是怎么来的
在发送请求之前,JavaScript 会先从页面输入框里读取内容:
title: document.querySelector('#title').value,
content: document.querySelector('#content').value,
这两个值就是从 DOM 里面读取出来的,也就是页面上标题和正文输入框里的内容。
然后组成一个 JavaScript 对象:
const body = {
title: document.querySelector('#title').value,
content: document.querySelector('#content').value,
};
再通过:
JSON.stringify(body)
转换成 JSON 字符串,放进 HTTP Request Body。
所以这一小段链路可以理解成:
页面输入框
↓
document.querySelector(...).value
↓
JavaScript 对象 body
↓
JSON.stringify(body)
↓
HTTP Request Body
3.3.2 fetch 请求的路由
可以看到这里请求的是:
/api/articles
这一部分涉及后端具体方法的匹配,留到第 4 节里面再讲,这里先留个印象。
4 Router
4.1 理解 Router 的作用
接下来看到:
app/routers/articles.py
这个文件就是我们接下来要讲的 Router。
完整代码如下:
"""HTTP routing layer for article endpoints."""
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from .. import schemas, services
from ..database import get_db
router = APIRouter(
prefix="/api/articles",
tags=["articles"],
)
@router.post(
"",
response_model=schemas.ArticleResponse,
status_code=status.HTTP_201_CREATED,
)
def create_article(
payload: schemas.ArticleCreate,
db: Session = Depends(get_db),
):
return services.create_article(db=db, payload=payload)
@router.get(
"",
response_model=list[schemas.ArticleResponse],
)
def get_articles(
db: Session = Depends(get_db),
):
return services.list_articles(db)
用比较简单的话来说,APIRouter 可以把一组相关接口组织在一起。
这里:
router = APIRouter(
prefix="/api/articles",
tags=["articles"],
)
通过:
prefix="/api/articles"
给这一组文章相关接口统一加上:
/api/articles
这个路径前缀。
顺便说一下:
tags=["articles"]
主要是用来在 Swagger 这类接口文档页面里做分类显示,在这里不参与请求匹配。
4.1.1 Router 想要使用,需要先在 main 里面注册
app.include_router(articles_router)
这个语句就是把 articles_router 注册到主 FastAPI 应用。
如果没有注册,这组 Router 就不会真正成为主应用的一部分,对应的请求也就匹配不到这里。
4.2 Router 里面的两个接口
这个 Router 里面有两个接口,一个是 POST,一个是 GET。
为了方便看清楚结构,分开来看。
4.2.1 POST 接口
@router.post(
"",
response_model=schemas.ArticleResponse,
status_code=status.HTTP_201_CREATED,
)
def create_article(
payload: schemas.ArticleCreate,
db: Session = Depends(get_db),
):
return services.create_article(db=db, payload=payload)
最上面的:
@router.post("")
说明下面这个函数处理的是一个 POST 请求。
其中:
""
表示这个接口在公共前缀 /api/articles 的基础上,没有再增加额外子路径。
所以完整路径还是:
/api/articles
也就是说:
POST /api/articles
→ create_article()
如果以后写成:
@router.post("/draft")
那么完整路径就是:
/api/articles/draft
也就是:
POST /api/articles/draft
再看下面两项:
response_model=schemas.ArticleResponse
status_code=status.HTTP_201_CREATED
它们分别表示:
response_model
→ 规定响应数据结构
status_code
→ 规定成功响应的 HTTP 状态码
例如:
ArticleResponse
→ 返回的数据应该符合这个结构
201 Created
→ 告诉客户端资源创建成功
接下来是:
def create_article(
payload: schemas.ArticleCreate,
db: Session = Depends(get_db),
):
return services.create_article(db=db, payload=payload)
这一段定义了具体的接口处理函数。
Router 本身更多负责 HTTP 层的组织和转发:
- 通过 Schema 的类型标注描述请求和响应数据;
- 通过
Depends(get_db)获取数据库 Session; - 再把真正的业务处理交给 Service。
至于 ArticleCreate、Depends(get_db) 和 Service 里面到底发生了什么,就留到下一篇继续看。
4.2.2 GET 接口
@router.get(
"",
response_model=list[schemas.ArticleResponse],
)
def get_articles(
db: Session = Depends(get_db),
):
return services.list_articles(db)
这里的路径也是:
/api/articles
但是 HTTP 方法变成了:
GET
所以它和前面的 POST 接口虽然 URL 一样,却可以对应不同的处理函数。
4.3 FastAPI 如何把请求匹配到具体处理函数
FastAPI 主要根据两个信息判断请求应该进入哪个处理函数:
HTTP Method
+
完整 URL 路径
例如这个 Demo 里:
GET /api/articles
→ get_articles()
POST /api/articles
→ create_article()
虽然它们的路径都是:
/api/articles
但是 HTTP 方法不同:
GET
POST
所以 FastAPI 可以判断应该执行哪个处理函数。
至于 FastAPI 内部具体是怎么完成路由匹配的,这一篇就先不往下钻了。
4.4 用浏览器直接访问 /api/articles
直接在浏览器地址栏访问:
http://127.0.0.1:8000/api/articles
可以看到之前创建过的文章数据:

回头再看 GET 接口:
@router.get(
"",
response_model=list[schemas.ArticleResponse],
)
def get_articles(
db: Session = Depends(get_db),
):
return services.list_articles(db)
这其实就是触发了这个 GET 接口的结果。
之所以可以直接在浏览器地址栏里访问,是因为浏览器本身也是一个 HTTP 客户端。
直接输入一个网址访问时,浏览器会发出 GET 请求,所以:
GET /api/articles
就会匹配到:
get_articles()
因此可以直接看到这个接口返回的数据。
这也说明:
浏览器地址栏比较适合直接测试 GET 接口。
如果想发送 POST、DELETE,或者需要携带自定义 Request Body,就需要使用 fetch、Swagger、Postman 等方式构造请求。
5 总结
到这里,一次请求的前半程已经比较清楚了:
用户点击“创建文章”
↓
<form> 触发 submit
↓
JavaScript 捕获事件
↓
收集 title / content
↓
组成 JavaScript 对象 body
↓
JSON.stringify(body)
↓
fetch 构造 HTTP POST 请求
↓
POST /api/articles
↓
FastAPI 根据 Method + Path
↓
匹配 create_article()
到这里,请求已经成功从浏览器进入了 FastAPI 对应的 Router 函数。
但是 Router 里面还有两个看起来有点“凭空出现”的东西:
payload: ArticleCreate
为什么能直接拿到前端发来的 JSON?
还有:
db: Session = Depends(get_db)
这个 db 又是从哪里来的?
这些就是下一篇继续要看的内容。
评论