用类视图组织接口
文章接口从“查看列表”扩展到创建、修改和删除后,同一资源的处理逻辑分散在多个函数里。本专题比较函数式处理函数与 HTTPEndpoint,再把文章操作收束到对应的类中。
类视图依 HTTP 方法分发到 get、post、put、delete 等方法。它是组织代码的方式,处理请求和返回响应的基本规则没有变化。示例代码见下文。
启动 05_article_class_views.py 后,可运行 class_views_demo.py 查看状态码与响应体。
函数视图与类视图
先把同一个简单接口分别写成函数和类,比较路由注册与处理函数的对应关系。
先从最简单的首页接口开始。
函数视图写法
示例文件:01_homepage_endpoint.py。运行:uvicorn 01_homepage_endpoint:app --reload。
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route
async def homepage(request):
return PlainTextResponse("首页")
routes = [
Route("/", homepage),
]
app = Starlette(routes=routes)类视图写法
同样的功能,也可以写成:
from starlette.applications import Starlette
from starlette.endpoints import HTTPEndpoint
from starlette.responses import PlainTextResponse
from starlette.routing import Route
class HomepageEndpoint(HTTPEndpoint):
async def get(self, request):
return PlainTextResponse("首页")
routes = [
Route("/", HomepageEndpoint),
]
app = Starlette(routes=routes)两种写法做的是同一件事:
访问 /
↓
返回 首页区别只在代码组织方式。
两种写法的区别
函数式写法:
简单直接
适合独立接口类视图写法:
更适合把同一资源的多个操作放在一起按方法处理 GET 与 POST
一个 HTTPEndpoint 可以按 HTTP 方法分发。方法名必须使用小写,且每个方法仍要返回响应对象。
现在看一个更适合类视图的例子:同一路径既要处理 GET,也要处理 POST。
示例文件:02_article_list_endpoint.py。运行:uvicorn 02_article_list_endpoint:app --reload。
from starlette.applications import Starlette
from starlette.endpoints import HTTPEndpoint
from starlette.responses import JSONResponse
from starlette.routing import Route
ARTICLES = [
{"id": 1, "title": "认识 Starlette 应用对象"},
{"id": 2, "title": "学习 Request 请求对象"},
]
class ArticleListEndpoint(HTTPEndpoint):
async def get(self, request):
return JSONResponse({"items": ARTICLES})
async def post(self, request):
request_data = await request.json()
title = request_data.get("title", "未命名文章")
new_article = {
"id": len(ARTICLES) + 1,
"title": title,
}
ARTICLES.append(new_article)
return JSONResponse(
{
"message": "文章创建成功",
"article": new_article,
},
status_code=201,
)
routes = [
Route("/articles", ArticleListEndpoint),
]
app = Starlette(routes=routes)
if __name__ == '__main__':
import uvicorn
uvicorn.run("02_article_list_endpoint:app", host='127.0.0.1', port=8000, reload=True)这一个类同时处理两个请求方法:
GET /articles → get()
POST /articles → post()也就是说:
同一个路径
不同请求方法
统一放在一个类里方法名使用小写
HTTPEndpoint 默认识别的是这些小写方法名:
get
post
put
delete不要写成:
GET
POST返回响应对象
即使写成类视图,方法最后仍然要返回响应对象。
例如:
return JSONResponse(...)这一点和函数式 endpoint 没有区别。
读取文章编号
路径参数仍从请求对象读取。文章不存在时,需要返回明确的错误;下一专题会统一处理错误格式。
接下来把路径参数也放进类视图里。
示例文件:03_article_detail_endpoint.py。运行:uvicorn 03_article_detail_endpoint:app --reload。
from starlette.applications import Starlette
from starlette.endpoints import HTTPEndpoint
from starlette.exceptions import HTTPException
from starlette.responses import JSONResponse
from starlette.routing import Route
ARTICLES = [
{"id": 1, "title": "认识 Starlette 应用对象"},
{"id": 2, "title": "学习 Request 请求对象"},
{"id": 3, "title": "学习 Response 响应对象"},
]
class ArticleDetailEndpoint(HTTPEndpoint):
async def get(self, request):
article_id = request.path_params["article_id"]
for article in ARTICLES:
if article["id"] == article_id:
return JSONResponse(article)
raise HTTPException(status_code=404, detail="文章不存在")
routes = [
Route("/articles/{article_id:int}", ArticleDetailEndpoint),
]
app = Starlette(routes=routes)
if __name__ == '__main__':
import uvicorn
uvicorn.run("03_article_detail_endpoint:app", host='127.0.0.1', port=8000, reload=True)这里虽然写成了类视图,但路径参数读取方式没有变:
request.path_params["article_id"]也就是说:
类视图只是组织方式变化
读取 request 的方法并没有变处理不存在的文章
如果没找到文章,就继续使用前面学过的异常处理方式:
raise HTTPException(status_code=404, detail="文章不存在")加入修改与删除操作
把 put 和 delete 加入文章资源后,再判断类视图是否让相关操作更易查找。
现在继续扩展到更完整的资源操作。
示例文件:04_article_crud_endpoint.py。运行:uvicorn 04_article_crud_endpoint:app --reload。
from starlette.applications import Starlette
from starlette.endpoints import HTTPEndpoint
from starlette.exceptions import HTTPException
from starlette.responses import JSONResponse
from starlette.routing import Route
ARTICLES = [
{"id": 1, "title": "认识 Starlette 应用对象"},
{"id": 2, "title": "学习 Request 请求对象"},
{"id": 3, "title": "学习 Response 响应对象"},
]
class ArticleDetailEndpoint(HTTPEndpoint):
async def get(self, request):
article = self.find_article(request.path_params["article_id"])
if article is None:
raise HTTPException(status_code=404, detail="文章不存在")
return JSONResponse(article)
async def put(self, request):
article = self.find_article(request.path_params["article_id"])
if article is None:
raise HTTPException(status_code=404, detail="文章不存在")
request_data = await request.json()
article["title"] = request_data.get("title", article["title"])
return JSONResponse({"message": "文章修改成功", "article": article})
async def delete(self, request):
article = self.find_article(request.path_params["article_id"])
if article is None:
raise HTTPException(status_code=404, detail="文章不存在")
ARTICLES.remove(article)
return JSONResponse({"message": "文章删除成功", "article": article})
def find_article(self, article_id):
for article in ARTICLES:
if article["id"] == article_id:
return article
return None
routes = [
Route("/articles/{article_id:int}", ArticleDetailEndpoint),
]
app = Starlette(routes=routes)
if __name__ == '__main__':
import uvicorn
uvicorn.run("04_article_crud_endpoint:app", host='127.0.0.1', port=8000, reload=True)这一个类里现在有三个方法:
get() 处理查看详情
put() 处理修改文章
delete() 处理删除文章这样和“文章详情资源”本身的结构就比较一致。
按资源组织操作
这就是类视图最常见的使用场景:
围绕同一个资源
有多个请求方法
希望把逻辑集中到一起选择函数视图或类视图
可以先用这个简单判断:
函数式 endpoint 更适合:
首页
说明页
跳转页
独立的小接口类视图 endpoint 更适合:
同一路径下有 GET、POST
同一资源下有 GET、PUT、DELETE
希望把相关逻辑集中在一个类里重点不是“哪种更高级”,而是“哪种更清晰”。
文章接口完整案例
完整案例用类视图承载文章列表与详情操作。
目标包括:
首页
文章列表 + 创建文章
文章详情 + 修改文章 + 删除文章
后台文章管理示例文件:05_article_class_views.py。运行:uvicorn 05_article_class_views:app --reload。
from starlette.applications import Starlette
from starlette.endpoints import HTTPEndpoint
from starlette.exceptions import HTTPException
from starlette.responses import JSONResponse, PlainTextResponse
from starlette.routing import Route
ARTICLES = [
{"id": 1, "title": "认识 Starlette 应用对象"},
{"id": 2, "title": "学习 Request 请求对象"},
{"id": 3, "title": "学习 Response 响应对象"},
]
class HomepageEndpoint(HTTPEndpoint):
async def get(self, request):
return PlainTextResponse("首页")
class ArticleListEndpoint(HTTPEndpoint):
async def get(self, request):
return JSONResponse({"items": ARTICLES})
async def post(self, request):
request_data = await request.json()
title = request_data.get("title", "未命名文章")
new_article = {"id": len(ARTICLES) + 1, "title": title}
ARTICLES.append(new_article)
return JSONResponse({"message": "文章创建成功", "article": new_article}, status_code=201)
class ArticleDetailEndpoint(HTTPEndpoint):
async def get(self, request):
article = self.find_article(request.path_params["article_id"])
if article is None:
raise HTTPException(status_code=404, detail="文章不存在")
return JSONResponse(article)
async def put(self, request):
article = self.find_article(request.path_params["article_id"])
if article is None:
raise HTTPException(status_code=404, detail="文章不存在")
request_data = await request.json()
article["title"] = request_data.get("title", article["title"])
return JSONResponse({"message": "文章修改成功", "article": article})
async def delete(self, request):
article = self.find_article(request.path_params["article_id"])
if article is None:
raise HTTPException(status_code=404, detail="文章不存在")
ARTICLES.remove(article)
return JSONResponse({"message": "文章删除成功", "article": article})
def find_article(self, article_id):
for article in ARTICLES:
if article["id"] == article_id:
return article
return None
class AdminArticlesEndpoint(HTTPEndpoint):
async def get(self, request):
return PlainTextResponse("后台文章管理")
routes = [
Route("/", HomepageEndpoint),
Route("/articles", ArticleListEndpoint),
Route("/articles/{article_id:int}", ArticleDetailEndpoint),
Route("/admin/articles", AdminArticlesEndpoint),
]
app = Starlette(routes=routes)
if __name__ == '__main__':
import uvicorn
uvicorn.run("05_article_class_views:app", host='127.0.0.1', port=8000, reload=True)1、访问首页
访问:
http://127.0.0.1:8000/2、查看文章列表
访问:
http://127.0.0.1:8000/articles3、创建文章
向下面地址发送 POST JSON:
http://127.0.0.1:8000/articles4、查看文章详情
访问:
http://127.0.0.1:8000/articles/15、修改文章
向下面地址发送 PUT JSON:
http://127.0.0.1:8000/articles/16、删除文章
向下面地址发送 DELETE:
http://127.0.0.1:8000/articles/17、后台文章管理
访问:
http://127.0.0.1:8000/admin/articles案例重点
这个案例重点不是业务复杂,而是帮助你看清楚:
同一资源的多个请求方法
如何集中到同一个类里同时也要记住:
类视图只是组织方式
不会自动替你完成业务校验、数据库操作或权限控制案例源码
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route
from starlette.endpoints import HTTPEndpoint
# async def homepage(request):
# return PlainTextResponse("欢迎学习 Endpoint 类视图")
#
#
# routes = [
# Route("/", homepage),
# ]
class HomepageEndpoint(HTTPEndpoint):
async def get(self, request):
return PlainTextResponse("首页")
routes = [
Route("/", HomepageEndpoint),
]
app = Starlette(routes=routes)
if __name__ == '__main__':
import uvicorn
uvicorn.run("01_homepage_endpoint:app", host='127.0.0.1', port=8000, reload=True)import requests
BASE_URL = "http://127.0.0.1:8000"
def test_list_articles():
response = requests.get(f"{BASE_URL}/articles", timeout=5)
print("GET /articles", response.status_code, response.json())
def test_create_article():
response = requests.post(
f"{BASE_URL}/articles", json={"title": "类视图新文章"}, timeout=5
)
print("POST /articles", response.status_code, response.json())
def test_update_article():
response = requests.put(
f"{BASE_URL}/articles/1", json={"title": "修改后的标题"}, timeout=5
)
print("PUT /articles/1", response.status_code, response.json())
def test_delete_article():
response = requests.delete(f"{BASE_URL}/articles/2", timeout=5)
print("DELETE /articles/2", response.status_code, response.json())
if __name__ == "__main__":
test_list_articles()
test_create_article()
test_update_article()
test_delete_article()