条件视图处理
HTTP 客户端可以发送一些头部信息,用来告诉服务器它们已经看过的资源的副本情况。这通常在检索网页(使用 HTTP 的 GET
请求)时使用,以避免发送客户端已经检索过的内容数据。然而,这些相同的头部信息可以用于所有 HTTP 方法(POST
,PUT
,DELETE
等)。
针对每个从 Django 视图返回的页面(响应),它可能提供了两种HTTP headers:ETag
header 和 Last-Modified
header。这些 headers 是 HTTP 的可选项。他们可以在视图函数里设置,或者依赖 ConditionalGetMiddleware 中间件来设置 ETag
header 。
当客户端下次请求相同资源时,它可能会发送一个类似 If-Modified-Since 或 If-Unmodified-Since 的头部,其中包含了上次发送的最后修改时间的日期,或者 If-Match 或 If-None-Match,其中包含了最后的 ETag
。如果页面的当前版本与客户端发送的 ETag
匹配,或者资源没有被修改,可以发送一个 304 状态码,而不是一个完整的响应,告诉客户端没有变化。根据头部的不同,如果页面已经被修改或者与客户端发送的 ETag
不匹配,可能会返回一个 412 状态码(前提条件失败)。
当你需要更多的控制,你可以使用针对每个视图的条件处理函数。
条件装饰器
有时(事实上,相当经常),你可以创建函数来快速计算资源的 ETag 值或最后修改时间,而不需要 执行构建完整视图所需的所有计算。Django 可以使用这些函数来提供视图处理的“提前退出”选项。告诉客户端内容自上次请求以来没有被修改,也许是这样。
这两个函数被当做参数传递到 django.views.decorators.http.condition
装饰器。这个装饰器使用两个函数(你只需要支持其中一个,如果你不能很快计算这两个数量)来判断 HTTP 请求的 headers 和这些资源是否匹配。如果它们没有匹配,会计算一份资源的副本,并调用视图。
条件装饰器如下:
condition(etag_func=None, last_modified_func=None)
用于计算 ETag 和最后修改时间的两个函数将接收传入的 request
对象和与它们帮助包装的视图函数中的相同参数,以相同的顺序。传递给 last_modified_func
的函数应返回一个标准的日期时间值,指定资源上次修改的时间,如果资源不存在则返回 None
。传递给 etag
装饰器的函数应返回表示资源的 ETag 的字符串,如果资源不存在则返回 None
。
如果它们没有通过视图设置并且请求的方法是安全的(GET
和 HEAD
),那么装饰器就会在请求上设置 ETag
和 Last-Modified
headers 。
用一个例子来解释如何有效地使用这个功能。假设你已经有了这些模型,代表一个博客系统:
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)
...
如果在首页正在显示最新的博客文章,只会在有新博客文章时改变,你可以很快地计算最后修改时间。你需要与该博客关联的每篇文章的最新发布时间。一种方法是这样做的:
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 客户端将向你发送哪一个headers,因此需要同时处理这两个headers。然而,有时候只有一个值易于计算,Django 提供的装饰器处理只有 ETag 或 只有 last-modified 的计算。
django.views.decorators.http.etag
和 django.views.decorators.http.last_modified
装饰器和 condition
装饰器一样传递相同的函数类型。它们的签名是这样的:
etag(etag_func)
last_modified(last_modified_func)
我们可以使用其中一个装饰器来编写更早一些的那个只使用last-modified函数的例子:
@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/
。 - 服务端用一些带有
"abcd1234"
的 ETag 响应一些内容。 - 客户端发送一个 HTTP
PUT
请求到/foo/
来更新一些资源。它也发送If-Match: "abcd1234"
header 来指定它准备更新的版本。 - 服务端通过计算 ETag(与
GET
请求计算的方式相同,使用相同的函数)来检查资源是否被修改。如果资源被改变,它将返回 412 状态码,意思是 “先决条件失败” 。 - 客户端在收到一个412响应后会发送一个
GET
请求到/foo/
,在更新它之前用来寻找内容更新的版本。
重要的是这个例子显示的是相同函数在所有情形下可以被用来计算ETag和最后一次修改。事实上,你应该使用相同的函数,这样相同的值会被实时返回。
具有不安全请求方法的验证 headers
condition
装饰器仅为安全的 HTTP 方法(即 GET
和 HEAD
)设置验证器标头(ETag
和 Last-Modified
)。如果您希望在其他情况下返回它们,请在视图中设置它们。请参阅 RFC 9110#section-9.3.4 了解在响应使用 PUT
与 POST
进行的请求时设置验证器标头的区别。
对比中间件的条件处理
Django 通过 django.middleware.http.ConditionalGetMiddleware 提供了有条件的 GET
处理。虽然易于使用,但是中间件在高级用法上是有限制的:
- 它可被全局应用于项目的所有视图。
- 它不会阻止你生成响应,这样代价可能很昂贵。
- 它只适合HTTP 的
GET
请求。
你应该为你的特殊需求选择最合适的工具。如果你有方法可以很迅速地计算 ETags 和 修改时间,并且如果一些视图花了一些时间去生成内容,那么你应该在这个文档中考虑使用 condition
装饰器描述。如果所有事务都运行的非常快了,那么坚持使用中间件。如果视图没有变动,那么发送回客户端的网络流量将仍然会减少。