条件视图处理 — Django 文档

来自菜鸟教程
Django/docs/3.2.x/topics/conditional-view-processing
跳转至:导航、​搜索

条件视图处理

HTTP 客户端可以发送许多标头来告诉服务器他们已经看到的资源副本。 这通常用于检索网页(使用 HTTP GET 请求)以避免发送客户端已经检索到的所有数据。 但是,相同的标头可用于所有 HTTP 方法(POSTPUTDELETE 等)。

对于 Django 从视图发回的每个页面(响应),它可能提供两个 HTTP 标头:ETag 标头和 Last-Modified 标头。 这些标头在 HTTP 响应中是可选的。 它们可以由您的视图函数设置,或者您可以依靠 ConditionalGetMiddleware 中间件来设置 ETag 标头。

当客户端下一次请求相同的资源时,它可能会发送一个标头,例如 If-modified-sinceIf-unmodified-since ,包含上次发送修改时间的日期,或 If-matchIf-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

如果视图尚未设置 ETagLast-Modified 标头,并且请求的方法是安全的(GETHEAD ])。

最好通过示例来解释如何有用地使用此功能。 假设你有这对模型,代表一个小型博客系统:

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.etagdjango.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

如果您想测试这两个先决条件,那么尝试链接 etaglast_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 装饰器不仅对 GETHEAD 请求有用(在这种情况下,HEAD 请求与 GET 请求相同) . 它还可以用于检查 POSTPUTDELETE 请求。 在这些情况下,想法不是返回“未修改”的响应,而是告诉客户端他们试图更改的资源在此期间已被更改。

例如,考虑客户端和服务器之间的以下交换:

  1. 客户端请求 /foo/
  2. 服务器响应一些带有 "abcd1234" ETag 的内容。
  3. 客户端向 /foo/ 发送 HTTP PUT 请求以更新资源。 它还发送 If-Match: "abcd1234" 标头以指定它尝试更新的版本。
  4. 服务器通过与对 GET 请求相同的方式计算 ETag(使用相同的函数)来检查资源是否已更改。 如果资源已经改变了,它会返回一个412状态码,意思是“前置条件失败”。
  5. 客户端在收到 412 响应后向 /foo/ 发送 GET 请求,以在更新内容之前检索内容的更新版本。

此示例显示的重要一点是,在所有情况下都可以使用相同的函数来计算 ETag 和上次修改值。 实际上,您 应该 使用相同的函数,以便每次都返回相同的值。

具有非安全请求方法的验证器标头

condition 装饰器只为安全的 HTTP 方法设置验证器标头(ETagLast-Modified),即 GETHEAD。 如果您希望在其他情况下返回它们,请在您的视图中设置它们。 请参阅 RFC 7231#section-4.3.4 以了解设置验证器标头以响应 PUTPOST 发出的请求之间的区别.


与中间件条件处理的比较

Django 通过 django.middleware.http.ConditionalGetMiddleware 提供有条件的 GET 处理。 虽然适用于许多情况,但中间件在高级使用方面存在局限性:

  • 它全局应用于项目中的所有视图。
  • 它不会使您免于生成响应,这可能很昂贵。
  • 它仅适用于 HTTP GET 请求。

您应该在此处为您的特定问题选择最合适的工具。 如果您有办法快速计算 ETag 和修改时间,并且某些视图需要一段时间来生成内容,您应该考虑使用本文档中描述的 condition 装饰器。 如果一切都已经运行得相当快,坚持使用中间件,如果视图没有改变,发送回客户端的网络流量仍然会减少。