Skip to content

用类视图组织接口 ​

文章接口从“查看列表”扩展到创建、修改和删除后,同一资源的处理逻辑分散在多个函数里。本专题比较函数式处理函数与 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。

python
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)

类视图写法 ​

同样的功能,也可以写成:

python
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)

两种写法做的是同一件事:

text
访问 /
↓
返回 首页

区别只在代码组织方式。

两种写法的区别 ​

函数式写法:

text
简单直接
适合独立接口

类视图写法:

text
更适合把同一资源的多个操作放在一起

函数视图与 HTTPEndpoint 类视图架构对比

按方法处理 GET 与 POST ​

一个 HTTPEndpoint 可以按 HTTP 方法分发。方法名必须使用小写,且每个方法仍要返回响应对象。

现在看一个更适合类视图的例子:同一路径既要处理 GET,也要处理 POST。

示例文件:02_article_list_endpoint.py。运行:uvicorn 02_article_list_endpoint:app --reload。

py
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)

这一个类同时处理两个请求方法:

text
GET  /articles  → get()
POST /articles  → post()

也就是说:

text
同一个路径
不同请求方法
统一放在一个类里

方法名使用小写 ​

HTTPEndpoint 默认识别的是这些小写方法名:

python
get
post
put
delete

不要写成:

python
GET
POST

返回响应对象 ​

即使写成类视图,方法最后仍然要返回响应对象。

例如:

python
return JSONResponse(...)

这一点和函数式 endpoint 没有区别。

HTTPEndpoint.dispatch 方法反射与 405 状态机机制

读取文章编号 ​

路径参数仍从请求对象读取。文章不存在时,需要返回明确的错误;下一专题会统一处理错误格式。

接下来把路径参数也放进类视图里。

示例文件:03_article_detail_endpoint.py。运行:uvicorn 03_article_detail_endpoint:app --reload。

py
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)

这里虽然写成了类视图,但路径参数读取方式没有变:

python
request.path_params["article_id"]

也就是说:

text
类视图只是组织方式变化
读取 request 的方法并没有变

处理不存在的文章 ​

如果没找到文章,就继续使用前面学过的异常处理方式:

python
raise HTTPException(status_code=404, detail="文章不存在")

加入修改与删除操作 ​

把 put 和 delete 加入文章资源后,再判断类视图是否让相关操作更易查找。

现在继续扩展到更完整的资源操作。

示例文件:04_article_crud_endpoint.py。运行:uvicorn 04_article_crud_endpoint:app --reload。

py
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)

这一个类里现在有三个方法:

text
get()    处理查看详情
put()    处理修改文章
delete() 处理删除文章

这样和“文章详情资源”本身的结构就比较一致。

按资源组织操作 ​

这就是类视图最常见的使用场景:

text
围绕同一个资源
有多个请求方法
希望把逻辑集中到一起

RESTful 资源收敛:单类承载 CRUD 全生命周期

选择函数视图或类视图 ​

可以先用这个简单判断:

函数式 endpoint 更适合:

text
首页
说明页
跳转页
独立的小接口

类视图 endpoint 更适合:

text
同一路径下有 GET、POST
同一资源下有 GET、PUT、DELETE
希望把相关逻辑集中在一个类里

重点不是“哪种更高级”,而是“哪种更清晰”。

函数视图 vs 类视图选型决策矩阵

文章接口完整案例 ​

完整案例用类视图承载文章列表与详情操作。

目标包括:

text
首页
文章列表 + 创建文章
文章详情 + 修改文章 + 删除文章
后台文章管理

示例文件:05_article_class_views.py。运行:uvicorn 05_article_class_views:app --reload。

py
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、访问首页

访问:

text
http://127.0.0.1:8000/

2、查看文章列表

访问:

text
http://127.0.0.1:8000/articles

3、创建文章

向下面地址发送 POST JSON:

text
http://127.0.0.1:8000/articles

4、查看文章详情

访问:

text
http://127.0.0.1:8000/articles/1

5、修改文章

向下面地址发送 PUT JSON:

text
http://127.0.0.1:8000/articles/1

6、删除文章

向下面地址发送 DELETE:

text
http://127.0.0.1:8000/articles/1

7、后台文章管理

访问:

text
http://127.0.0.1:8000/admin/articles

案例重点 ​

这个案例重点不是业务复杂,而是帮助你看清楚:

text
同一资源的多个请求方法
如何集中到同一个类里

同时也要记住:

text
类视图只是组织方式
不会自动替你完成业务校验、数据库操作或权限控制

案例源码 ​

py
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)
py
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()