条件视图处理

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 值或资源的上次修改时间, without 需要进行构建全景图所需的所有计算。然后,Django可以使用这些函数为视图处理提供“早期救助”选项。可能告诉客户端内容自上次请求以来没有被修改过。

这两个函数作为参数传递给 django.views.decorators.http.condition 装饰符。这个修饰符使用两个函数(如果不能轻松快速地计算两个数量,只需提供一个)来计算HTTP请求中的头是否与资源中的头匹配。如果它们不匹配,则必须计算资源的新副本,并调用普通视图。

这个 condition 修饰符的签名如下:

condition(etag_func=None, last_modified_func=None)

用于计算ETag和上次修改时间的两个函数将传递给传入的 request 对象和相同的参数,其顺序与它们帮助包装的视图函数相同。传递的函数 last_modified_func 应返回指定上次修改资源的时间的标准DateTime值,或 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响应上。

只计算一个值的快捷方式

作为一般规则,如果可以提供函数来计算 both 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 如果要测试这两个前提条件,请使用decorator。但是,这会导致不正确的行为。

# Bad code. Don't do this!
@etag(etag_func)
@last_modified(last_modified_func)
def my_view(request):
    ...


# End of bad code.

第一个修饰符对第二个修饰符一无所知,可能会回答响应没有被修改,即使第二个修饰符会决定是否修改。这个 condition decorator同时使用两个回调函数来计算要采取的正确操作。

将decorator与其他HTTP方法一起使用

这个 condition decorator不仅对 GETHEAD 请求 (HEAD 请求与 GET 在这种情况下)。它还可用于检查 POSTPUTDELETE 请求。在这些情况下,这个想法并不是返回“未修改”的响应,而是告诉客户他们试图更改的资源在这段时间内已被更改。

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

  1. 客户端请求 /foo/ .

  2. 服务器用ETag响应一些内容 "abcd1234" .

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

  4. 服务器通过计算etag来检查资源是否已更改,方法与对 GET 请求(使用相同的功能)。如果资源 has 更改后,返回412状态码,表示“前提条件失败”。

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

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

具有非安全请求方法的验证程序头

这个 condition 修饰器仅设置验证器标头 (ETagLast-Modified )对于安全的HTTP方法,即 GETHEAD 。如果您希望在其他情况下退回它们,请在您的视图中设置它们。看见 RFC 9110#section-9.3.4 了解设置验证器标头以响应使用 PUTPOST

与中间件条件处理的比较

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

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

  • 这不会节省您生成响应的时间,这可能很昂贵。

  • 它只适用于HTTP GET 请求。

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