条件视图处理¶
HTTP 客户端可以发送多个头部来告诉服务器它们已经看过的资源副本。这通常用于检索网页(使用 HTTP GET
请求)以避免发送客户端已经检索到的内容的所有数据。但是,相同的头部可用于所有 HTTP 方法(POST
、PUT
、DELETE
等)。
对于 Django 从视图返回的每个页面(响应),它可能会提供两个 HTTP 头部:ETag
头部和 Last-Modified
头部。这些头部在 HTTP 响应中是可选的。它们可以由您的视图函数设置,或者您可以依靠 ConditionalGetMiddleware
中间件来设置 ETag
头部。
当客户端下次请求相同资源时,它可能会发送一个头部,例如 If-Modified-Since 或 If-Unmodified-Since,包含上次发送的修改时间日期,或者 If-Match 或 If-None-Match,包含上次发送的 ETag
。如果页面的当前版本与客户端发送的 ETag
匹配,或者如果资源未被修改,则可以发送 304 状态码而不是完整响应,告诉客户端没有任何更改。根据头部,如果页面已被修改或与客户端发送的 ETag
不匹配,则可能会返回 412 状态码(前提条件失败)。
当您需要更细粒度的控制时,您可以使用每个视图的条件处理函数。
condition
装饰器¶
有时(实际上,非常频繁地)您可以创建函数来快速计算 ETag 值或资源的上次修改时间,**无需**执行构建完整视图所需的所有计算。然后,Django 可以使用这些函数为视图处理提供“早期退出”选项。例如,告诉客户端内容自上次请求以来未被修改。
这两个函数作为参数传递给 django.views.decorators.http.condition
装饰器。此装饰器使用这两个函数(如果不能轻松快速地计算这两个值,则只需要提供一个)来确定 HTTP 请求中的头部是否与资源上的头部匹配。如果它们不匹配,则必须计算资源的新副本,并调用您的普通视图。
condition
装饰器的签名如下所示
condition(etag_func=None, last_modified_func=None)
用于计算 ETag 和上次修改时间的两个函数将传递传入的 request
对象以及与它们正在帮助包装的视图函数相同的参数,并以相同的顺序。传递给 last_modified_func
的函数应返回一个标准日期时间值,指定资源上次修改的时间,或者如果资源不存在,则返回 None
。传递给 etag
装饰器的函数应返回一个字符串,表示资源的 ETag,或者如果它不存在,则返回 None
。
如果视图尚未设置 ETag
和 Last-Modified
头部,并且请求的方法是安全的(GET
或 HEAD
),则装饰器会在响应中设置这些头部。
可能最好通过示例来解释如何有效地使用此功能。假设您有以下一对模型,表示一个小型博客系统
import datetime
from django.db import models
class Blog(models.Model): ...
class Entry(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
published = models.DateTimeField(default=datetime.datetime.now)
...
如果显示最新博客条目的首页仅在您添加新的博客条目时才会更改,则可以非常快速地计算上次修改时间。您需要与该博客关联的每个条目的最新 published
日期。一种方法是
def latest_entry(request, blog_id):
return Entry.objects.filter(blog=blog_id).latest("published").published
然后,您可以使用此函数为您的首页视图提供未更改页面的早期检测
from django.views.decorators.http import condition
@condition(last_modified_func=latest_entry)
def front_page(request, blog_id): ...
小心装饰器的顺序
当 condition()
返回条件响应时,它下面的任何装饰器都将被跳过,并且不会应用于响应。因此,任何需要同时应用于常规视图响应和条件响应的装饰器都必须位于 condition()
之上。特别是,vary_on_cookie()
、vary_on_headers()
和 cache_control()
应该放在前面,因为 RFC 9110 要求它们设置的头部存在于 304 响应中。
仅计算一个值的快捷方式¶
作为一般规则,如果您可以提供函数来计算 ETag 和上次修改时间,则应这样做。您不知道任何给定的 HTTP 客户端会向您发送哪些头部,因此请准备好处理两者。但是,有时只有一个值易于计算,并且 Django 提供了仅处理 ETag 或仅处理上次修改计算的装饰器。
django.views.decorators.http.etag
和 django.views.decorators.http.last_modified
装饰器传递与 condition
装饰器相同类型的函数。它们的签名是
etag(etag_func)
last_modified(last_modified_func)
我们可以使用这些装饰器之一来编写前面的示例,该示例仅使用上次修改函数
@last_modified(latest_entry)
def front_page(request, blog_id): ...
…或者
def front_page(request, blog_id): ...
front_page = last_modified(latest_entry)(front_page)
在测试两个条件时使用 condition
¶
对于某些人来说,如果要测试两个前提条件,尝试链接 etag
和 last_modified
装饰器可能看起来更好。但是,这会导致错误的行为。
# Bad code. Don't do this!
@etag(etag_func)
@last_modified(last_modified_func)
def my_view(request): ...
# End of bad code.
第一个装饰器不知道第二个装饰器,并且可能会回答响应未修改,即使第二个装饰器会确定否则也是如此。condition
装饰器同时使用这两个回调函数来确定要采取的正确操作。
将装饰器与其他 HTTP 方法一起使用¶
condition
装饰器不仅对 GET
和 HEAD
请求有用(在这种情况下,HEAD
请求与 GET
相同)。它也可用于提供对 POST
、PUT
和 DELETE
请求的检查。在这些情况下,目的不是返回“未修改”响应,而是告诉客户端他们尝试更改的资源在此期间已更改。
例如,考虑客户端和服务器之间的以下交换
客户端请求
/foo/
。服务器以一些内容作为响应,其 ETag 为
"abcd1234"
。客户端发送 HTTP
PUT
请求到/foo/
以更新资源。它还发送If-Match: "abcd1234"
头部以指定它尝试更新的版本。服务器通过以与
GET
请求相同的方式计算 ETag(使用相同的函数)来检查资源是否已更改。如果资源已更改,它将返回 412 状态码,表示“前提条件失败”。客户端发送一个
GET
请求到/foo/
,在收到 412 响应后,以检索内容的更新版本,然后再更新它。
这个例子说明的重要一点是,在所有情况下,都可以使用相同的函数来计算 ETag 和最后修改值。事实上,你**应该**使用相同的函数,以便每次都返回相同的值。
带有非安全请求方法的验证器标头
condition
装饰器仅为安全 HTTP 方法(即 GET
和 HEAD
)设置验证器标头(ETag
和 Last-Modified
)。如果你希望在其他情况下返回它们,请在你的视图中设置它们。请参阅RFC 9110 第 9.3.4 节,了解响应使用 PUT
与 POST
发出的请求设置验证器标头的区别。
与中间件条件处理的比较¶
Django 通过 django.middleware.http.ConditionalGetMiddleware
提供条件 GET
处理。虽然适用于许多情况,但中间件在高级用法方面存在局限性
它全局应用于项目中的所有视图。
它不会让你免于生成响应,而生成响应可能代价很高。
它仅适用于 HTTP
GET
请求。
你应该为你的特定问题选择最合适的工具。如果你有办法快速计算 ETag 和修改时间,并且某些视图需要一段时间才能生成内容,则应考虑使用本文档中描述的 condition
装饰器。如果所有内容都运行得相当快,则坚持使用中间件,如果视图没有更改,则仍然会减少发送回客户端的网络流量。