条件视图处理

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 状态码而不是完整响应,告诉客户端没有任何更改。根据头部,如果页面已被修改或与客户端发送的 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

如果视图尚未设置 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 9110 要求它们设置的头部存在于 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(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. 服务器以一些内容作为响应,其 ETag 为 "abcd1234"

  3. 客户端发送 HTTP PUT 请求到 /foo/ 以更新资源。它还发送 If-Match: "abcd1234" 头部以指定它尝试更新的版本。

  4. 服务器通过以与 GET 请求相同的方式计算 ETag(使用相同的函数)来检查资源是否已更改。如果资源更改,它将返回 412 状态码,表示“前提条件失败”。

  5. 客户端发送一个 GET 请求到 /foo/,在收到 412 响应后,以检索内容的更新版本,然后再更新它。

这个例子说明的重要一点是,在所有情况下,都可以使用相同的函数来计算 ETag 和最后修改值。事实上,你**应该**使用相同的函数,以便每次都返回相同的值。

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

condition 装饰器仅为安全 HTTP 方法(即 GETHEAD)设置验证器标头(ETagLast-Modified)。如果你希望在其他情况下返回它们,请在你的视图中设置它们。请参阅RFC 9110 第 9.3.4 节,了解响应使用 PUTPOST 发出的请求设置验证器标头的区别。

与中间件条件处理的比较

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

  • 它全局应用于项目中的所有视图。

  • 它不会让你免于生成响应,而生成响应可能代价很高。

  • 它仅适用于 HTTP GET 请求。

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

返回顶部