月薪40k+测试·开发同步认可的FastAPI:Python 世界里最受欢迎的异步框架_sanic fastapi

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新软件测试全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注软件测试)
img

正文

a2: List[str] = Query(),
b: List[str] = Query()**):
return {“a1”: a1, “a2”: a2, “b”: b}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

我们访问一下,看看结果:

首先 “a2” 和 “b” 都是对应列表,然后 “a1” 只获取了最后一个值。另外可能有人觉得我们这样有点啰嗦,在函数声明中可不可以这样写呢?

@app.get(**"/items"**) **async** **def** **read\_items**(**a1: **str**, a2: List[**str**], b: List[**str**]**): **return** {"a1": a1, "a2": a2, "b": b}

对于 a1 是可以的,但是 a2 和 b 不行。对于类型为 list 的查询参数,无论有没有默认值,你都必须要显式的加上 Query 来表示必传参数。如果允许为 None(或者有默认值)的话,那么应该这么写:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from typing import Optional, List
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@app.get(“/items”)
async def read_items(a1: str,
a2: Optional[List[str]] = Query(None),
b: List[str] = Query(
[“1”, “嘿嘿”])):
return {“a1”: a1, “a2”: a2, “b”: b}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

给参数起别名

问题来了,假设我们定义的查询参数名叫 item-query,那么由于它要体现在函数参数中、而这显然不符合 Python 变量的命名规范,这个时候要怎么做呢?答案是起一个别名。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from typing import Optional, List
from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@app.get(“/items”)
async def read_items(# 通过 url 的时候使用别名即可
item1: Optional[str] = Query(**None, alias=“item-query”
),
item2: str = Query(“哈哈”, alias=“@@@@”),
item3: str = Query(…, alias=“$$$$”) # item3 是必传的**):
return {“item1”: item1, “item2”: item2, “item3”: item3}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

数值检测

Query 不仅仅支持对字符串的校验,还支持对数值的校验,里面可以传递 gt、ge、lt、le 这几个参数,相信这几个参数不用说你也知道是干什么的,我们举例说明:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, Query
import uvicorn

app = FastAPI()

@app.get(“/items”)
async def read_items(# item1 必须大于 5
item1: int = Query(
…, gt=5**),

item2 必须小于等于 7

item2: int = Query(**…, le=7**),

item3 必须必须等于 10

item3: int = Query(…, ge=10, le=10**)):
return {“item1”: item1, “item2”: item2, “item3”: item3}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

Query 还是比较强大了,当然内部还有一些其它的参数是针对 docs 交互文档的,有兴趣可以自己了解一下。

路径参数和数据校验

查询参数数据校验使用的是 Query,路径参数数据校验使用的是 Path,两者的使用方式一模一样,没有任何区别。

`from fastapi import FastAPI, Path
import uvicorn

app = FastAPI()

@app.get(“/items/{item-id}”)
async def read_items(item_id: int = Path(…, alias=“item-id”)):
return {“item_id”: item_id}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

因为路径参数是必须的,它是路径的一部分,所以我们应该使用 … 将其标记为必传参数。当然即使不这么做也无所谓,因为指定了默认值也用不上,因为路径参数不指定压根就匹配不到相应的路由。至于一些其它的校验,和查询参数一模一样,所以这里不再赘述了。

不过我们之前说过,路径参数应该在查询参数的前面,尽管 FastAPI 没有这个要求,但是这样写明显更舒服一些。但是问题来了,如果路径参数需要指定别名,但是某一个查询参数不需要,这个时候就会出现问题:

`@app.get(“/items/{item-id}”)
async def read_items(q: str,
item_id: int = Path(
…, alias=“item-id”)):

return {“item_id”: item_id, “q”: q}`

显然此时 Python 的语法就决定了 item_id 就必须放在 q 的后面,当然这么做是完全没有问题的,FastAPI 对参数的先后顺序没有任何要求,因为它是通过参数的名称、类型和默认值声明来检测参数,而不在乎参数的顺序。但此时我们就要让 item_id 在 q 的前面要怎么做呢?

`@app.get(“/items/{item-id}”)
async def read_items(*, item_id: int = Path(…, alias=“item-id”**),
q: str**):

return {“item_id”: item_id, “q”: q}`

此时就没有问题了,通过将第一个参数设置为 *,使得 item_id 和 q 都必须通过关键字传递,所以此时默认参数在非默认参数之前也是允许的。当然我们也不需要担心 FastAPI 传参的问题,你可以认为它所有的参数都是通过关键字参数的方式传递的。

Request

Request 是什么?首先我们知道任何一个请求都对应一个 Request 对象,请求的所有信息都在这个 Request 对象中,FastAPI 也不例外。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, Request
import uvicorn

app = FastAPI()

@app.get(“/girl/{user_id}”)
async def read_girl(user_id: str,
request: Request
):
“”“路径参数是必须要体现在参数中,但是查询参数可以不写了
因为我们定义了 request: Request,那么请求相关的所有信息都会进入到这个 Request 对象中”“”
header = request.headers # 请求头
method = request.method # 请求方法
cookies = request.cookies # cookies
query_params = request.query_params # 查询参数
return {“name”: query_params.get(“name”), “age”: query_params.get(“age”), “hobby”: query_params.getlist(“hobby”)}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

我们通过 Request 对象可以获取所有请求相关的信息,我们之前当参数传递不对的时候,FastAPI 会自动帮我们返回错误信息,但通过 Request 我们就可以自己进行解析、自己指定返回的错误信息了。

Response

既然有 Request,那么必然会有 Response,尽管我们可以直接返回一个字典,但 FastAPI 实际上会帮我们转成一个 Response 对象。

Response 内部接收如下参数:

  • content:返回的数据
  • status_code:状态码
  • headers:返回的请求头
  • media_type:响应类型(就是 HTML 中 Content-Type,只不过这里换了个名字)
  • background:接收一个任务,Response 在返回之后会自动异步执行(这里不做介绍,后面会说)

举个栗子:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, Request, Response
import uvicorn
import orjson

app = FastAPI()

@app.get(“/girl/{user_id}”)
async def read_girl(user_id: str,
request: Request
):
query_params = request.query_params # 查询参数
data = {“name”: query_params.get(“name”), “age”: query_params.get(“age”), “hobby”: query_params.getlist(“hobby”)}

实例化一个 Response 对象

response = Response(

content,我们需要手动转成 json 字符串,如果直接返回字典的话,那么在包装成 Response 对象的时候会自动帮你转

orjson.dumps(data),

status_code,状态码

201,

headers,响应头

{“Token”: “xxx”},

media_type,就是 HTML 中的 Content-Type

“application/json”,
)

如果想设置 cookie 的话,那么通过 response.set_cookie 即可

删除 cookie 则是 response.delete_cookie

return response

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

通过 Response 我们可以实现请求头、状态码、cookie 等自定义。

另外除了 Response 之外还有很多其它类型的响应,它们都在 fastapi.responses 中,比如:FileResponse、HTMLResponse、PlainTextResponse 等等。它们都继承了 Response,只不过会自动帮你设置响应类型,举个栗子:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI
from fastapi.responses import Response, HTMLResponse
import uvicorn

app = FastAPI()

@app.get(“/index”)
async def index():

response1 = HTMLResponse(“

你好呀

”)
response2 = Response(“

你好呀

”, media_type=“text/html”)

以上两者是等价的,在 HTMLResponse 中会自动将 media_type 设置成 text/html

return response1

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

其它类型的请求与响应

FastAPI 除了 GET 请求之外,还支持其它类型,比如:POST、PUT、DELETE、OPTIONS、HEAD、PATCH、TRACE 等等。而常见的也就 GET、POST、PUT、DELETE,介绍完了 GET,我们来说一说其它类型的请求。

显然对应 POST、PUT 等类型的请求,我们必须要能够解析出请求体,并且能够构造出响应体。

Model

在 FastAPI 中,请求体和响应体都对应一个 Model,举个栗子:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from typing import Optional, List
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()

class Girl(BaseModel):
“”“数据验证是通过 pydantic 实现的,我们需要从中导入 BaseModel,然后继承它”“”
name: str
age: Optional[str] = None
length: float
hobby: List[str] # 对于 Model 中的 List[str] 我们不需要指定 Query(准确的说是 Field)

@app.post(“/girl”)
async def read_girl(girl: Girl):

girl 就是我们接收的请求体,它需要通过 json 来传递,并且这个 json 要有上面的四个字段(age 可以没有)

通过 girl.xxx 的方式我们可以获取和修改内部的所有属性

return dict(girl) # 直接返回 Model 对象也是可以的

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

除了使用这种方式之外,我们还可以使用之前说的 Request 对象:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, Request
import uvicorn

app = FastAPI()

@app.post(“/girl”)
async def read_girl(request: Request):

是一个协程,所以需要 await

data = await request.body()
print(data)

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

首先我们在使用 requests 模块发送 post 请求的时候可以通过 data 参数传递、也可以通过 json 参数。

当通过 json={"name": "satori", "age": 16, "length": 155.5} 传递的时候,会将其转成 json 字符串进行传输,程序中的 print 打印如下:

b'{"name": "satori", "age": 16, "length": 155.5}'

如果我们是用过 data 参数发请求的话(值不变),那么会将其拼接成 k1=v1&k2=v2 的形式再进行传输(相当于表单提交,后面说),程序中打印如下:

b'name=satori&age=16&length=155.5'

所以我们看到 await request.body() 得到的就是最原始的字节流,而除了 await request.body() 之外还有一个 await request.json(),只是后者在内部在调用了前者拿到字节流之后、自动帮你 loads 成了字典。因此使用 await request.json() 也侧面要求,我们必须在发送请求的时候必须使用 json 参数传递(传递的是字典转成的 json、所以也能解析成字典),否则使用 await request.json() 是无法正确解析的。

路径参数、查询参数、请求体

我们可以将三者混合在一起:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from typing import Optional, List
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()

class Girl(BaseModel):
name: str
age: Optional[str] = None
length: float
hobby: List[str]

@app.post(“/girl/{user_id}”)
async def read_girl(user_id,
q: str,
girl: Girl
):

return {“user_id”: user_id, “q”: q, **dict(girl)}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

里面我们同时指定了路径参数、查询参数和请求体,FastAPI 依然是可以正确区分的,当然我们也可以使用 Request 对象。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from typing import Optional, List, Dict
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()

@app.post(“/girl/{user_id}”)
async def read_girl(user_id,
request: Request
):

q = request.query_params.get(“q”)
data: Dict = await request.json()
data.update({“user_id”: user_id, “q”: q})
return data

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

可以自己测试一下,仍然是可以正确返回的。

多个请求体参数

我们上面的只接收一个 json 请求体,如果是接收两个呢?

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from typing import Optional, List
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()

class Girl(BaseModel):
name: str
age: Optional[str] = None

class Boy(BaseModel):
name: str
age: int

@app.post(“/boy_and_girl”)
async def read_boy_and_girl(girl: Girl,
boy: Boy
):
return {“girl”: dict(girl), “boy”: dict(boy)}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

此时在传递的时候,应该按照如下方式传递:

应该将两个 json 嵌套在一起,组成一个更大的 json,至于 key 就是我们的函数参数名。因此这种方式其实就等价于:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from typing import Optional, List, Dict
from fastapi import FastAPI, Request, Response
from pydantic import BaseModel
import uvicorn

app = FastAPI()

class BoyAndGirl(BaseModel):
girl: Dict
boy: Dict

@app.post(“/boy_and_girl”)
async def read_boy_and_girl(boy_and_girl: BoyAndGirl):
return dict(boy_and_girl)

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

这种方式也是可以实现的,只不过就字典内部的字典的不可进行限制了。当然啦,我们仍然可以使用 Request 对象,得到字典之后自己再进行判断,因为对于 json 而言,内部的字段可能是会变的,而且最关键的是字段可能非常多。这个时候,我个人更倾向于使用 Request 对象。

Form 表单

我们调用 requests.post,如果参数通过 data 传递的话,则相当于提交了一个 form 表单,那么在 FastAPI 中可以通过 await request.form() 进行获取,注意:内部同样是先调用 await request.body()。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, Request, Response
import uvicorn

app = FastAPI()

@app.post(“/girl”)
async def girl(request: Request):

此时 await request.json() 报错,因为是通过 data 参数传递的,相当于 form 表单提交

如果是通过 json 参数传递,那么 await request.form() 会得到一个空表单

form = await request.form()
return [form.get(“name”), form.getlist(“age”)]

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

当然我们也可以通过其它方式:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, Form
import uvicorn

app = FastAPI()

@app.post(“/user”)
async def get_user(username: str = Form(),
password: str = Form(
)):
return {“username”: username, “password”: password}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

像 Form 表单,查询参数、路径参数等等,都可以和 Request 对象一起使用,像上面的例子,如果我们多定义一个 request: Request,那么我们仍然可以通过 await request.form() 拿到相关的表单信息。所以如果你觉得某个参数不适合类型注解,那么你可以单独通过 Request 对象进行解析。

文件上传

那么问题来了,FastAPI 如何接收用户的文件上传呢?首先如果想使用文件上传功能,那么你必须要安装一个包 python-multipart,直接 pip install python-multipart 即可。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, File, UploadFile
import uvicorn

app = FastAPI()

@app.post(“/file1”)
async def file1(file: bytes = File()):
return f"文件长度: {len(file)}"

@app.post(“/file2”)
async def file1(file: UploadFile = File()):
return f"文件名: {file.filename}, 文件大小: {len(await file.read())}"

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

我们看到一个直接获取字节流,另一个是获取类似于文件句柄的对象。如果是多个文件上传要怎么做呢?

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from typing import List
from fastapi import FastAPI, UploadFile, File
import uvicorn

app = FastAPI()

@app.post(“/file”)
async def file(files: List[UploadFile] = File()):
“”“指定类型为列表即可”“”
for idx, f in enumerate(files):
files[idx] = f"文件名: {f.filename}, 文件大小: {len(await f.read())}"
return files

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

此时我们就实现了 FastAPI 文件上传,当然文件上传并不影响我们处理表单,可以自己试一下同时处理文件和表单。

返回静态资源

下面来看看 FastAPI 如何返回静态资源,首先我们需要安装 aiofiles,直接 pip 安装即可。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
import uvicorn

app = FastAPI()

name 参数只是起一个名字,FastAPI 内部使用

app.mount(“/static”, StaticFiles(directory=r"C:\Users\satori\Desktop\bg"), name=“static”)

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

浏览器输入:localhost:5555/static/1.png,那么会返回 C:\Users\satori\Desktop\bg 下的 1.png 文件。

错误处理

错误处理也是一个不可忽视的点,错误有很多种,比如:

  • 客户端没有足够的权限执行此操作
  • 客户端没有访问某个资源的权限
  • 客户端尝试访问一个不存在的资源
  • ...

这个时候我们应该将错误通知相应的客户端,这个客户端可以浏览器、代码程序、IoT 设备等等。

但是就我个人而言,更倾向于使用 Response 对象,将里面的  status_code 设置为 404,然后在返回的 json 中指定错误信息。不过 FastAPI 内部也提供了一些异常类:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, HTTPException
import uvicorn

app = FastAPI()

@app.get(“/items/{item_id}”)
async def read_item(item_id: str):
if item_id != “foo”:

里面还可以传入 headers 设置响应头

raise HTTPException(status_code=404, detail=“item 没有发现”)
return {“item”: “bar”}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

HTTPException 是一个普通的 Python 异常类(继承了 Exception),它携带了 API 的相关信息,既然是异常,那么我们不能 return、而是要 raise。

个人觉得这个不是很常用,至少我本人很少用这种方式返回错误,因为它能够携带的信息太少了。

自定义异常

FastAPI 内部提供了一个 HTTPException,但是我们也可以自定义,但是注意:我们自定义完异常之后,还要定义一个 handler,将异常和 handler 绑定在一起,然后引发该异常的时候就会触发相应的 handler。

`from fastapi import FastAPI, Request
from fastapi.responses import ORJSONResponse
import uvicorn

app = FastAPI()

class ASCIIException(Exception):
“”“何もしません”“”

通过装饰器的方式,将 ASCIIException 和 ascii_exception_handler 绑定在一起

@app.exception_handler(ASCIIException)
async def ascii_exception_handler(request: Request, exc: ASCIIException):
“”“当引发 ASCIIException 的时候,会触发 ascii_exception_handler 的执行
同时会将 request 和 exception 传过去”“”
return ORJSONResponse(status_code=404, content={“code”: 404, “message”: “你必须传递 ascii 字符串”})

@app.get(“/items/{item_id}”)
async def read_item(item_id: str):
if not item_id.isascii():
raise ASCIIException
return {“item”: f"get {item_id}"}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

关于 Request、Response,我们除了可以通过 fastapi 进行导入,还可以通过 starlette 进行导入,因为 fastapi 的路由映射是通过 starlette 来实现的。当然我们直接从 fastapi 中进行导入即可。

自定义 404

当访问一个不存在的 URL,我们应该提示用户,比如:您要找到页面去火星了。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from fastapi.exceptions import StarletteHTTPException
import uvicorn

app = FastAPI()

@app.exception_handler(StarletteHTTPException)
async def not_found(request, exc):
return ORJSONResponse({“code”: 404, “message”: “您要找的页面去火星了。。。”})

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

此时当我们访问一个不存在的 URL 时,就会返回我们自定义的 JSON 字符串。

后台任务

如果一个请求耗时特别久,那么我们可以将其放在后台执行,而 FastAPI 已经帮我们做好了这一步。我们来看一下:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

import time
from fastapi import FastAPI, BackgroundTasks
import uvicorn

app = FastAPI()

def send_email(email: str, message: str = “”):
“”“发送邮件,假设耗时三秒”“”
time.sleep(3)
print(f"三秒之后邮件发送给 {email!r}, 邮件信息: {message!r}")

@app.get(“/user/{email}”)
async def order(email: str, bg_tasks: BackgroundTasks):
“”“这里需要多定义一个参数
此时任务就被添加到后台,当 Response 对象返回之后触发”“”
bg_tasks.add_task(send_email, email, message=“这是一封邮件”)

我们在之前介绍 Response 的时候说过,里面有一个参数 background

所以我们也可以将任务放在那里面

因此我们还可以:

return Response(orjson.dumps({“message”: “邮件发送成功”}), background=BackgroundTask(send_email, email, message=“这是一封邮件”))

return {“message”: “邮件发送成功”}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

首先请求肯定是成功的:

然后响应的 3 秒,终端会出现如下打印:

所以此时任务是被后台执行了的,注意:任务是在响应返回之后才后台执行。

APIRouter

APIRouter 类似于 Flask 中的蓝图,可以更好的组织大型项目,举个栗子:

在我当前的工程目录中有一个 app 目录和一个 main.py,其中 app 目录中有一个 app01.py,然后我们看看它们是如何组织的。

`# app/app01.py
from fastapi import APIRouter

router = APIRouter(prefix=“/router”)

以后访问的时候要通过 /router/v1 来访问

@router.get(“/v1”)
async def v1():
return {“message”: “hello world”}

main.py

from fastapi import FastAPI
from app.app01 import router
import uvicorn

app = FastAPI()

将 router 注册到 app 中,相当于 Flask 中的 register_blueprint

app.include_router(router)

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

然后可以在外界通过 /router/v1 的方式来访问。

中间件

中间件在 web 开发中可以说是非常常见了,说白了中间件就是一个函数或者一个类。在请求进入视图函数之前,会先经过中间件(被称为请求中间件),而在中间件里面,我们可以对请求进行一些预处理,或者实现一个拦截器等等;同理当视图函数返回响应之后,也会经过中间件(被称为响应中间件),在中间件里面,我们也可以对响应进行一些润色。

自定义中间件

在 FastAPI 里面也支持像 Flask 一样自定义中间件,但是 Flask 里面有请求中间件和响应中间件,但是在 FastAPI 里面这两者合二为一了,我们看一下用法。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, Request, Response
import uvicorn
import orjson

app = FastAPI()

@app.get(“/”)
async def view_func(request: Request):
return {“name”: “古明地觉”}

@app.middleware(“http”)
async def middleware(request: Request, call_next):
“”"
定义一个协程函数,然后使用 @app.middleware(“http”) 装饰,即可得到中间件
“”"

请求到来时会先经过这里的中间件

if request.headers.get(“ping”, “”) != “pong”:
response = Response(content=orjson.dumps({“error”: “请求头中缺少指定字段”}),
media_type=“application/json”,
status_code=404)

当请求头中缺少 “ping”: “pong”,在中间件这一步就直接返回了,就不会再往下走了

所以此时就相当于实现了一个拦截器

return response

然后,如果条件满足,则执行 await call_next(request),关键是这里的 call_next

如果该中间件后面还有中间件,那么 call_next 就是下一个中间件;如果没有,那么 call_next 就是对应的视图函数

这里显然是视图函数,因此执行之后会拿到视图函数返回的 Response 对象

所以我们看到在 FastAPI 中,请求中间件和响应中间件合在一起了

response: Response = await call_next(request)

这里我们在设置一个响应头

response.headers[“status”] = “success”
return response

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

我们可以测试一下:

测试结果也印证了我们的结论。

内置的中间件

通过自定义中间件,我们可以在不修改视图函数的情况下,实现功能的扩展。但是除了自定义中间件之外,FastAPI 还提供了很多内置的中间件。

`app = FastAPI()

要求请求协议必须是 https 或者 wss,如果不是,则自动跳转

from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
app.add_middleware(HTTPSRedirectMiddleware)

请求中必须包含 Host 字段,为防止 HTTP 主机报头攻击,并且添加中间件的时候,还可以指定一个 allowed_hosts,那么它是干什么的呢?

假设我们有服务 a.example.com, b.example.com, c.example.com

但我们不希望用户访问 c.example.com,就可以像下面这么设置,如果指定为 [“*”],或者不指定 allow_hosts,则表示无限制

from starlette.middleware.trustedhost import TrustedHostMiddleware
app.add_middleware(TrustedHostMiddleware, allowed_hosts=[“a.example.com”, “b.example.com”])

如果用户的请求头的 Accept-Encoding 字段包含 gzip,那么 FastAPI 会使用 GZip 算法压缩

minimum_size=1000 表示当大小不超过 1000 字节的时候就不压缩了

from starlette.middleware.gzip import GZipMiddleware
app.add_middleware(TrustedHostMiddleware, minimum_size=1000)`

除了这些,还有其它的一些内置的中间件,可以自己查看一下,不过不是很常用。

CORS

CORS 过于重要,我们需要单独拿出来说。

CORS(跨域资源共享)是指浏览器中运行的前端里面拥有和后端通信的 JavaScript 代码,而前端和后端处于不同源的情况。源:协议(http、https)、域(baidu.com、app.com、localhost)以及端口(80、443、8000),只要有一个不同,那么就是不同源。比如下面都是不同的源:

  • http://localhost
  • https://localhost
  • http://localhost:8080

即使它们都是 localhost,但是它们使用了不同的协议或端口,所以它们是不同的源。假设你的前端运行在 localhost:8080,并且尝试与 localhost:5555 进行通信;然后浏览器会向后端发送一个 HTTP OPTIONS 请求,后端会发送适当的 headers 来对这个源进行授权;所以后端必须有一个 “允许的源” 列表,如果前端对应的源是被允许的,浏览器才会允许前端向后端发请求,否则就会出现跨域失败。

而默认情况下,前后端必须是在同一个源,如果不同源那么前端就会请求失败。而前后端分离早已成为了主流,因此跨域问题是必须要解决的。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn

app = FastAPI()
app.add_middleware(
CORSMiddleware,

允许跨域的源列表,例如 [“http://www.example.org”] 等等,[“*”] 表示允许任何源

allow_origins=[“*”],

跨域请求是否支持 cookie,默认是 False,如果为 True,allow_origins 必须为具体的源,不可以是 [“*”]

allow_credentials=False,

允许跨域请求的 HTTP 方法列表,默认是 [“GET”]

allow_methods=[“*”],

允许跨域请求的 HTTP 请求头列表,默认是 [],可以使用 [“*”] 表示允许所有的请求头

当然 Accept、Accept-Language、Content-Language 以及 Content-Type 总之被允许的

allow_headers=[“*”],

可以被浏览器访问的响应头, 默认是 [],一般很少指定

expose_headers=[“*”]

设定浏览器缓存 CORS 响应的最长时间,单位是秒。默认为 600,一般也很少指定

max_age=1000

)

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

以上即可解决跨域问题。

高阶操作

下面我们看一些 FastAPI 的高阶操作,这些操作有的不一定能用上,但用上了确实会方便许多。

其它的响应

返回 json 数据可以是:JSONResponse、UJSONResponse、ORJSONResponse,Content-Type 是 application/json;返回 html 是 HTMLResponse,Content-Type 是 text/html;返回 PlainTextResponse,Content-Type 是 text/plain。但是我们还可以有三种响应,分别是返回重定向、字节流、文件。

重定向

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI
from fastapi.responses import RedirectResponse
import uvicorn

app = FastAPI()

@app.get(“/index”)
async def index():
return RedirectResponse(“https://www.bilibili.com”)

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

页面中访问 /index 会跳转到 bilibili。

字节流

返回字节流需要使用异步生成器的方式:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import uvicorn

app = FastAPI()

async def some_video():
for i in range(5):
yield f"video {i} bytes ".encode(“utf-8”)

@app.get(“/index”)
async def index():
return StreamingResponse(some_video())

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

如果有文件对象,那么也是可以直接返回的。

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import uvicorn

app = FastAPI()

@app.get(“/index”)
async def index():
return StreamingResponse(open(“main.py”, encoding=“utf-8”))

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

文件

返回文件的话,还可以通过 FileResponse:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI
from fastapi.responses import FileResponse
import uvicorn

app = FastAPI()

@app.get(“/index”)
async def index():

filename 如果给出,它将包含在响应的 Content-Disposition 中。

return FileResponse(“main.py”, filename=“这不是main.py”)

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

可以自己发请求测试一下。

HTTP 验证

如果当用户访问某个请求的时候,我们希望其输入用户名和密码来确认身份的话该怎么做呢?

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI, Depends
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import uvicorn

app = FastAPI()

security = HTTPBasic()

@app.get(“/index”)
async def index(credentials: HTTPBasicCredentials = Depends(security)):
return {“username”: credentials.username, “password”: credentials.password}

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

当用户访问 /index 的时候,会提示输入用户名和密码:

输入完毕之后,信息会保存在 credentials,我们可以获取出来进行验证。

websocket

然后我们来看看 FastAPI 如何实现 websocket:

`# -*- coding:utf-8 -*-

@Author: komeiji satori

from fastapi import FastAPI
from fastapi.websockets import WebSocket
import uvicorn

app = FastAPI()

@app.websocket(“/ws”)
async def ws(websocket: WebSocket):
await websocket.accept()
while True:

websocket.receive_bytes()

websocket.receive_json()

data = await websocket.receive_text()
await websocket.send_text(f"收到来自客户端的回复: {data}")

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

然后我们通过浏览器进行通信:

`

Title

//如果连接成功, 会打印下面这句话, 否则不会打印
ws.onopen = function () {
console.log(‘连接成功’)
};

//接收数据, 服务端有数据过来, 会执行
ws.onmessage = function (event) {
console.log(event)
};

//服务端主动断开连接, 会执行.
//客户端主动断开的话, 不执行
ws.onclose = function () { }

`

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
ebSocket
import uvicorn

app = FastAPI()

@app.websocket(“/ws”)
async def ws(websocket: WebSocket):
await websocket.accept()
while True:

websocket.receive_bytes()

websocket.receive_json()

data = await websocket.receive_text()
await websocket.send_text(f"收到来自客户端的回复: {data}")

if name == “__main__”:
uvicorn.run(“main:app”, host=“0.0.0.0”, port=5555)`

然后我们通过浏览器进行通信:

`

Title

//如果连接成功, 会打印下面这句话, 否则不会打印
ws.onopen = function () {
console.log(‘连接成功’)
};

//接收数据, 服务端有数据过来, 会执行
ws.onmessage = function (event) {
console.log(event)
};

//服务端主动断开连接, 会执行.
//客户端主动断开的话, 不执行
ws.onclose = function () { }

`

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
[外链图片转存中…(img-YFgcQdXr-1713227613131)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值