条件视图处理 — Django 文档
条件视图处理
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状态码,而不是一个完整的响应,告诉客户端什么都没有改变了。 根据header,如果页面被修改或者与客户端发送的ETag
不匹配,可能会返回412状态码(Precondition Failed)。
当您需要更细粒度的控制时,您可以使用每个视图的条件处理函数。
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 72632[X] ] 要求他们设置的标头出现在 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 函数:
@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 的内容。 - 客户端向
/foo/
发送 HTTPPUT
请求以更新资源。 它还发送If-Match: "abcd1234"
标头以指定它尝试更新的版本。 - 服务器通过与对
GET
请求相同的方式计算 ETag(使用相同的函数)来检查资源是否已更改。 如果资源已经改变了,它会返回一个412状态码,意思是“前置条件失败”。 - 客户端在收到 412 响应后向
/foo/
发送GET
请求,以在更新内容之前检索内容的更新版本。
此示例显示的重要一点是,在所有情况下都可以使用相同的函数来计算 ETag 和上次修改值。 实际上,您 应该 使用相同的函数,以便每次都返回相同的值。
具有非安全请求方法的验证器标头
condition
装饰器只为安全的 HTTP 方法设置验证器标头(ETag
和 Last-Modified
),即 GET
和 HEAD
。 如果您希望在其他情况下返回它们,请在您的视图中设置它们。 请参阅 RFC 7231#section-4.3.4 以了解设置验证器标头以响应 PUT
与 POST
发出的请求之间的区别.
与中间件条件处理的比较
Django 通过 django.middleware.http.ConditionalGetMiddleware 提供简单直接的条件 GET
处理。 虽然易于使用且适用于多种情况,但中间件在高级使用方面存在局限性:
- 它全局应用于项目中的所有视图。
- 它不会使您免于生成响应,这可能很昂贵。
- 它仅适用于 HTTP
GET
请求。
您应该在此处为您的特定问题选择最合适的工具。 如果您有办法快速计算 ETag 和修改时间,并且某些视图需要一段时间来生成内容,您应该考虑使用本文档中描述的 condition
装饰器。 如果一切都已经运行得相当快,坚持使用中间件,如果视图没有改变,发送回客户端的网络流量仍然会减少。