Skip to main content

条件视图处理

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

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

当客户机下一次请求相同的资源时,它可能沿着包含它被发送的最后修改时间的日期的 If-modified-sinceIf-unmodified-since 的头部,或者包含它被发送的最后一个 ETagIf-matchIf-none-match 发送。如果页面的当前版本与客户机发送的 ETag 匹配,或者如果资源没有被修改,则可以发回304状态代码,而不是全部响应,告诉客户机没有改变。根据报头,如果页面已被修改或不匹配由客户端发送的 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 的函数应该返回一个指定资源被修改的最后一次的标准datetime值,如果该资源不存在,则返回 None。传递给 etag 装饰器的函数应该返回一个表示资源的 ETag 的字符串,如果它不存在,则返回 None

有用地使用此功能可能最好用一个示例解释。假设你有这对模型,代表一个简单的博客系统:

import datetime
from django.db import models

class Blog(models.Model):
    ...

class Entry(models.Model):
    blog = models.ForeignKey(Blog)
    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 7232 要求它们设置的报头存在于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 装饰器可能看起来更好。然而,这将导致不正确的行为。

# 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来检查资源是否已经改变。如果资源 has 改变,它将返回412状态码,意味着“前提条件失败”。

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

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

与中间件条件处理的比较

您可能注意到,Django已经通过 django.middleware.http.ConditionalGetMiddlewareCommonMiddleware 提供了简单和直接的条件 GET 处理。虽然当然易于使用并适用于许多情况,但这些中间件功能对高级用途有局限性:

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

  • 它们不会阻止您生成响应本身,这可能很昂贵

  • 它们仅适用于HTTP GET 请求。

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