02-视图、路由与中间件

发布日期:2024-10-04 修改时间:2024-10-04 阅读所需:25 分钟
django
python
web
backend

MVC 与 Django 应用结构

Django 诞生于 MVC 盛行的时代,这里为那些和我一样跳过 MVC 的朋友补充一下,MVC 三个概念的具体含义:

  1. Model:模型,泛指一切与数据相关的模型,你可以粗浅地将其理解为是 ORM,也可以是 Java 里面的那种 DTO、PO、DO 等;
  2. View:视图,就是用户能直接看到的部分都叫视图,比如你手机端上的各种应用界面、网页等,都可以算作是视图范畴;
  3. Controller:控制器,这个概念可能有点抽象,它就表示一种与用户(请求)交互的层级、逻辑或区域,你可以把它想象是一个工厂,在里面根据需要来组装不同的原材料从而生产出面向不同消费者的产品。
Diagram用户/浏览器Controller 控制器Model 模型View 视图HTTP请求处理请求返回数据组装数据渲染结果HTTP响应

但 Django 的概念稍微有点调整,它是 MTV 模式,它与 MVC 的差别在于后两者,其中:

  1. T 表示 Template 模板,其实也就是直接与 HTML 划上等号,毕竟 Django 最早主要也是用于开发新闻网站;
  2. V 还是表示 View 视图,但它实际就是 Controller 控制器。

为了避免混淆,我这里还是统一沿用 MVC 这个名词。

不过在如今前后端开发分离盛行的时代,视图层部分大多数时候都由前端开发工程师负责(比如前端框架的 Vue 读音就近似于 View)并在浏览器上进行渲染呈现,所以如果你只是一个纯后端工程师或者 CRUD Boy,那么完全可以跳过有关于模板部分的学习。

在上一章中我们使用了 manage.py 命令创建了一个 myapp 应用,从文件树结构中你依旧可以看到 Django 保留下来了 MVC 的文件命名习惯,只不过模板则是需要用户手动添加。

myapp
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

在实际开发中这个结构可以进一步自己调整,但我们更多时候会对 models.pyviews.py 文件操作,因为它们分别是应用的核心部分;前者主要涉及数据模型的逻辑,而后者则是涉及请求与响应的逻辑,这也是我们全篇的重点。

从视图到路由

在 Django 的视图函数中我们主要编写处理 HTTP 请求的逻辑。

现在我们在 myapp/views.py 中创建一个函数来处理一个简单的 HTTP 请求,并返回一个响应,它的代码如下所示:

myapp/views.py
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world!")

就这么简单:接收一个 request 对象,然后返回一个特定的 HttpResponse 响应对象。

也许你在使用 Django 之前已经接触过了 Flask 或者 FastAPI,你已经习惯了路由装饰器与对应的视图函数相结合:

app.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World!"}

但在 Django 项目中通常需要我们自己手动新建一个名为 urls.py 文件中对应用进行路由配置,配置通常都会保存在一个名为 urlpatterns 的变量中(约定俗成),它的代码如下所示:

myapp/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="index"),
]

简单来说,urls.py 就可以理解是对应应用的路由表,方便你对所有的路由进行管理。

紧接着还没完,我们需要到项目的同名文件夹下的进一步挂载到根节点上:

djangotutor/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('myapp/', include('myapp.urls')),
]

之后当我们启动 Django 服务器并在浏览器中打开 http://127.0.0.1:8000/myapp/ 时,你便可以看到我们的 index 视图函数的响应结果了:

Terminal window
curl http://127.0.0.1:8000/myapp/
Hello, world!

Django 这种从视图到路由的添加方式对于已习惯了装饰器方式的使用者来说会觉得繁琐,但这在有几十上百个路由的中大型项目中将会凸显优势,便于集中管理。

当然 Django 还允许你在配置时指定对应的路径参数,下面是一个来自于官方的示例:

from django.urls import path
from . import views
urlpatterns = [
path("articles/2003/", views.special_case_2003),
path("articles/<int:year>/", views.year_archive),
path("articles/<int:year>/<int:month>/", views.month_archive),
path("articles/<int:year>/<int:month>/<slug:slug>/", views.article_detail),
]

当我们访问不同层级下的路径时,Django 会自动匹配并将路径参数解析并传入到对应的视图函数中。

此外,除了 path() 函数之外,Django 还提供了 re_path() 函数,它和 path() 的功能是一样的,但是支持更复杂的正则表达式匹配:

from django.urls import path, re_path
from . import views
urlpatterns = [
path("articles/2003/", views.special_case_2003),
re_path(r"^articles/(?P<year>[0-9]{4})/$", views.year_archive),
re_path(r"^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$", views.month_archive),
re_path(
r"^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[\w-]+)/$",
views.article_detail,
),
]

在使用 (?P<...>) 这种命名组时,Django 会自动将匹配的值传入到对应的视图函数中,这些值可以在视图函数中通过 kwargs 参数来获取或者需要在视图函数签名中添加对应的参数来接收,比如:def article_detail(request, year, month, slug)

随着你的路由逐渐增多,那么清晰地划分或拆分路由就这项任务就会变得很重要。但 Django 提供了一个名为 include() 的函数来辅助你对路由进行拆分,直接看对应用法:

myapp/urls.py
from django.urls import path, include
from . import views
extrapatterns = [
path("order/", views.make_order),
path("payment/", views.make_payment),
]
urlpatterns = [
# ...
# other routes
path("extra/", include(extrapatterns)),
path("ticket/", include([
path("check/", views.check_ticket),
path("cancel/", views.cancel_ticket),
])),
path('article/', include('article.urls')),
]

你既可以直接传入一个已经包含了 path() 对象的列表,也可以传入一个包含对应模块的 URLconf 的字符串,Django 会自动为你整合这些路由。

如果你觉得这样划分可能还是有些麻烦,那么在使用 path()re_path()设定路由时还可以使用 name 参数来为对应的路由指定别名,这样就可以使用 reverse() 函数来进行反向匹配,帮助我们快速找到对应的 URL 路径,这在类似于使用指定链接或进行重定向时特别有用,下面是一个例子:

urls.py
from django.urls import path, include
from . import views
urlpatterns = [
path("", views.index, name="index"),
path("auth/", include([
path("login/", views.login, name="login"),
path("logout/", views.logout, name="logout"),
])),
]
# views.py
from django.http import HttpResponseRedirect, reverse, HttpResponse
def index(request):
if not request.user.is_authenticated:
return HttpResponseRedirect(reverse("login"))
return HttpResponse("Hello, world!")
def login(request):
if request.user.is_authenticated:
return HttpResponseRedirect(reverse("index"))
# handle login logic
return HttpResponse("Login")
def logout(request):
if not request.user.is_authenticated:
return HttpResponseRedirect(reverse("login"))
# handle logout logic
return HttpResponseRedirect(reverse("index"))

深入视图

了解了 Django 的路由配置之后,再让我们把话题转回到视图上。

获取请求信息

如果你有一定的计算机网络知识,那么你能快速理解或上手对大多数 Web 框架的路由视图或控制器使用方式,无非就是将来自于客户端请求在传输层之上的通过应用层协议传入服务器进行处理,而其中 HTTP 作为被广泛使用的应用层协议之一,则自然是为所有 Web 框架支持,包括 Django。

从我们上述的 index(request) 视图函数签名可以看出 Django 会将 HTTP 请求包装成一个特定的 HttpRequest 对象 传入。

那么通过 request 对象,我们可以拿到哪些信息呢?比如说最常用到的 HTTP 的请求方法、Cookie、HTTP 请求头、HTTP 请求体、路径以及查询参数等等。我们可以直接在视图函数中去通过 request 对象来进行操作:

myapp/views.py
from django.http import JsonResponse
def index(request):
method = request.method
cookies = request.COOKIES
headers = request.headers
body = request.body
path = request.path
query = request.GET
request_information = {
"method": method,
"cookies": cookies,
"headers": headers,
"body": body,
"path": path,
"query": query
}
return JsonResponse(request_information)

类视图

除了函数视图外,Django 还提供了强大的类视图(Class-Based Views,简称 CBV)系统,在这里我们快速看一个示例来了解为什么会有这类视图的写法存在:

from django.views import View
# 基础类视图
class UserView(View):
def get(self, request):
# 处理 GET 请求
return HttpResponse("GET request")
def post(self, request):
# 处理 POST 请求
return HttpResponse("POST request")
def dispatch(self, request, *args, **kwargs):
# 自定义处理请求时的额外逻辑
print("Before handling request")
return super().dispatch(request, *args, **kwargs)

在上面例子中假设我们有一个关于用户的需求场景,通过 GET 请求时是获取用户列表,通过 POST 请求时是创建用户。根据 RESTful 的设计思想,通常就会表现为同一个路由根据不同的方法来做出不同行为的区分:

GET /api/user
POST /api/user

如果是按照的函数视图写法那么就得要么写两个函数,要么在一个函数中根据请求方法来判断然后返回不同的响应结果。

# FastAPI 示例
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/user")
def get_user():
return {"message": "GET request"}
@app.post("/api/user")
def create_user():
return {"message": "POST request"}
# ------------------------------------
# Django 示例
from django.http import HttpResponse
def user(request):
if request.method == "GET":
return HttpResponse("GET request")
elif request.method == "POST":
return HttpResponse("POST request")
else:
return HttpResponse("Method Not Allowed", status=405)

可以看到相比函数视图,CBV 提供了更好的代码复用性和可扩展性。以 HTTP 方法为分离点帮助我们快速地将不同方法逻辑分离同时又能保持代码的简洁性。

只不过相比于函数视图,CBV 的对应的 URL 配置可能有点点小不同:

myapp/urls.py
from django.urls import path
from .views import UserView
urlpatterns = [
# ...
# other routes
path('user/', UserView.as_view()),
]

在向路由表添加 CBV 时,我们需要手动调用 as_view() 这个类方法,最后它将根据 HTTP 方法来调用 dispatch() 方法动态派发到同名的视图函数上。

更多的类视图

View 类是 Django 中类视图基类,但也额外提供了一些封装好的视图类,比如最常用的 ListViewDetailView 类,它们就可以帮助我们快速地完成常见的列表和详情页的逻辑。你可以直接从 django.views.generic 模块中导入它们。

假设我们现在有一个关于博客文章的模型 Article,我们将会有两个视图,一个是文章列表,一个是文章详情,在 Django 中就可以这么写:

from django.views.generic import ListView, DetailView
from .models import Article
from django.urls import reverse_lazy
# 文章列表视图
class ArticleListView(ListView):
model = Article
template_name = 'myapp/article_list.html'
context_object_name = 'articles'
paginate_by = 10
ordering = ['-created_at']
# 文章详情视图
class ArticleDetailView(DetailView):
model = Article
template_name = 'myapp/article_detail.html'
context_object_name = 'article'

然后我们到 myapp/urls.py 文件中添加两个路由,它们的代码如下所示:

myapp/urls.py
from django.urls import path
from .views import ArticleListView, ArticleDetailView
urlpatterns = [
# ...
# other routes
path('article/list/', ArticleListView.as_view(), name='article-list'),
path('article/<int:pk>/', ArticleDetailView.as_view(), name='article-detail'),
]

这样当我们访问 http://127.0.0.1:8000/myapp/article/list/ 时,就会显示文章列表,访问 http://127.0.0.1:8000/myapp/article/1/ 时,就会显示文章详情。

默认情况下,以上通用类视图都会和 HTML 模板文件结合到一起,由 Django 帮我们渲染到浏览器上,这也是 Django 的默认行为。如果是在前后端分离的场景下,就需要我们自己去手动扩展,后续我们可以直接使用第三方的 Django 扩展包来帮助我们更好地专注于 API 开发上而无需手动定制。

但在使用第三方扩展包之前先让我们来看看如何自己手动扩展。简单来说,你可以用 Mixin 类覆盖原有类的 render_to_response() 方法,然后按照 MRO 来摆放类的继承顺序即可。

MRO 是 Python 多重继承的一个特性,它是按照从左到右的继承顺序来进行属性或方法的查找,最先被找的属性或方法则会被优先使用。

from django.views.generic import ListView
from django.http import JsonResponse
from .models import Article
class JsonResponseMixin:
def render_to_response(self, context):
data = {
'count': len(context['object_list']),
'articles': list(context['object_list'].values())
}
return JsonResponse(data)
class ArticleListView(JsonResponseMixin, ListView):
model = Article
context_object_name = 'articles'
paginate_by = 10
ordering = ['-created_at']
class ArticleDetailView(JsonResponseMixin, DetailView):
model = Article
context_object_name = 'article'

于是乎我们的 Article 模型就能分别以 JSON 格式返回数据了。

中间件系统

最后还要讲一讲 Django 的中间件。它是我们在使用 Django 时会经常碰到的一个概念,可以帮助我们在请求和响应处理过程中做出一些额外的逻辑处理,减少重复性代码的同时提高代码的可维护性。

那么什么是中间件?中间件一词是由 Middleware 翻译而来,也可以叫中介层,是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的软件,应用软件可以借助中间件在不同的技术架构之间共享信息与资源。[1]

简单理解就是:在开发过程中用到组件、服务等都可以算作中间件范畴。比如我们在查询数据库前套一层 Redis 缓存,又或者面对某些长耗时任务先推到 Kafka 消息队列里,这里的 Redis 和 Kafka 就是中间件。

那么对于大多数具备了中间件机制的 Web 框架而言——比如 Django——你可以借助中间件来实现一些常见的功能,比如日志记录、用户鉴权等。

如何添加中间件

Django 开箱自带了一系列的中间件,你可以在 Django 工程项目的同名模块下找到对应的 settings.py 文件,当中的 MIDDLEWARE 一项配置就列出了 Django 默认的中间件有哪些:

djangotutor/settings.py
# other settings...
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# other settings...

可以看到它们都是以某个 包名.模块.中间件类名 的格式来进行引入,所以后续我们想要添加第三方库的中间件或者自定义的中间件时也如法炮制即可。

假设我们在 myapp 应用下创建了一个名为 middleware.py 的文件,然后在 settings.py 文件中添加如下配置:

djangotutor/settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'myapp.middleware.CustomMiddleware', # 自定义中间件
]

在 Django 添加中间件时,顺序很重要。因为 Django 的中间件机制就好像一个洋葱模型,由上自下层层嵌套,在执行时你可以把它想象成是一个 FIFO 队列(First In, First Out),因此最先添加的中间件会先被执行。

DiagramSecurityMiddleware1. __call__() before get_response2. process_view()3. get_response(request)4. process_template_response()/process_exception()5. __call__() after get_response...CustomMiddlewareView

所以如果你想把某个第三方或者自定义中间件的优先级调高,那么你最好将其放在最前面;而 Django 内置的中间件则有对应的顺序说明,一般情况下我们不会轻易调整。

你可能注意到了上图中标识了几个函数名称 __call__()process_view()get_response()process_template_response()/process_exception(),它们则是新版本 Django 中间件的流程 API,所以如果我们要自定义中间件就可以从它们开始入手。

自定义中间件

Django 提供了两种方式来让你自定义中间件,即函数中间件(Funcion-Based Middleware)和类中间件(Class-Based Middleware)。它们都需要接受一个 get_response 的可调用对象,并返回相应的响应结果。

现在假设我们需要一个用于记录请求信息的的中间件,每次都会记录包含请求的路径、HTTP 方法、请求 ID 等信息。

函数中间件

用函数式写法就需要你写成一个闭包的形式:

myapp/middleware.py
def LoggingMiddleware(get_response):
# 可以在这里写一些初始化配置的逻辑
mylog = somesdk.get_tracer()
def middleware(request):
# 可以在这里写一些处理请求/响应前的逻辑
# 比如:在日志记录器上添加一些上下文信息
nonlocal mylog
log = mylog.bind(request_id=uuid4(), method=request.method, path=request.path)
response = get_response(request)
# 可以在这里写一些处理请求/响应后**但在返回之前**的逻辑
# 比如:清理上下文信息避免干扰另一个请求
log.unbind()
return response
return middleware

接着只需要在你的 settings.py 中引入该中间件即可:

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'myapp.middleware.LoggingMiddleware',
]

类中间件

相比于函数中间件而言,类中间件就是传统 OOP 的写法,但逻辑基本类似。不过流程图上提到的 process_xxx 之类的钩子方法仅限于类中间件:

myapp/middleware.py
class LoggingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# 可以在这里写一些初始化配置的逻辑
self.mylog = somesdk.get_tracer()
def __call__(self, request):
# 可以在这里写一些处理请求/响应前的逻辑
# 比如:在日志记录器上添加一些上下文信息
log = self.mylog.bind(request_id=uuid4(), method=request.method, path=request.path)
response = self.get_response(request)
# 可以在这里写一些处理请求/响应后**但在返回之前**的逻辑
# 比如:清理上下文信息避免干扰另一个请求
log.unbind()
return response
def process_view(self, request, view_func, view_args, view_kwargs):
# 可以在这里写一些处理请求/响应前的逻辑,甚至修改入参函数等
view_kwargs['boom'] = True
self.mylog.info('process_view', view_func=view_func, view_args=view_args, view_kwargs=view_kwargs)
return view_func(request, *view_args, **view_kwargs)
def process_exception(self, request, exception):
exc = 'Oh no! Something went wrong!'
self.mylog.exception(exc, exc_info=exception)
raise ValueError(exc) from exception

总结

本章我们在了解了传统的 MVC 模式与 Django 的 MTV 模式后深入介绍了 Django 中的视图、路由与中间件这三个核心概念。

  • 视图。视图即处理 HTTP 请求的函数,可以是函数视图或者类视图。可以说我们所有的逻辑几乎都放在了视图中。
  • 路由。路由就是定义了 URL 和视图关系的表,在 Django 中主要以集中管理和配置的方式来组织你路由,这对于大型项目来说是十分方便的。
  • 中间件。中间件本质上是一种处理请求/响应前后的逻辑,在 Django 中可以是函数中间件或者类中间件,它们都需要接受一个 get_response 的可调用对象,并返回相应的响应结果。你可以基于中间件来拆分一些公共的逻辑,比如日志记录,权限验证等,避免在不同的视图中重复写相同的逻辑,提高代码的复用性和可维护性。

练习题

  1. 创建一个随机生成 5 个密码的接口 /generate_passwords,要求:

    1. 可接收指定密码长度的 length 参数,最短不小于 8,最长不超过 16,如果没有传该参数那么默认就返回长度为 8 的密码

    2. 返回一个 JSON 格式的响应,包含密码列表

    提示:可以使用 Python 的 secrets 模块来生成随机字符串密码

  2. 我现在需要实现一个通用的中间件 RequestMiddleware。它可以记录每次请求的路径、HTTP 方法和头部信息;同时,记录对应视图函数的处理时间,并为每个请求的响应新增一个请求 ID(使用 UUID),然后将它们作为 X-Process-TimeX-Request-ID 的头部信息添加到响应并返回。