条件视图处理

HTTP clients can send a number of headers to tell the server about copies of a resource that they have already seen. This is commonly used when retrieving a web page (using an HTTP GET request) to avoid sending all the data for something the client has already retrieved. However, the same headers can be used for all HTTP methods (POST, PUT, DELETE, etc.).

针对每个从 Django 视图返回的页面(响应),它可能提供了两种HTTP headers:ETag header 和 Last-Modified header。这些 headers 是 HTTP 的可选项。他们可以在视图函数里设置,或者依赖 ConditionalGetMiddleware 中间件来设置 ETag header 。

When the client next requests the same resource, it might send along a header such as either If-Modified-Since or If-Unmodified-Since, containing the date of the last modification time it was sent, or either If-Match or If-None-Match, containing the last ETag it was sent. If the current version of the page matches the ETag sent by the client, or if the resource has not been modified, a 304 status code can be sent back, instead of a full response, telling the client that nothing has changed. Depending on the header, if the page has been modified or does not match the ETag sent by the client, a 412 status code (Precondition Failed) may be returned.

当你需要更多的控制,你可以使用针对每个视图的条件处理函数。

条件装饰器

Sometimes (in fact, quite often) you can create functions to rapidly compute the ETag value or the last-modified time for a resource, without needing to do all the computations needed to construct the full view. Django can then use these functions to provide an "early bailout" option for the view processing. Telling the client that the content has not been modified since the last request, perhaps.

这两个函数被当做参数传递到 django.views.decorators.http.condition 装饰器。这个装饰器使用两个函数(你只需要支持其中一个,如果你不能很快计算这两个数量)来判断 HTTP 请求的 headers 和这些资源是否匹配。如果它们没有匹配,会计算一份资源的副本,并调用视图。

条件装饰器如下:

condition(etag_func=None, last_modified_func=None)

The two functions, to compute the ETag and the last modified time, will be passed the incoming request object and the same parameters, in the same order, as the view function they are helping to wrap. The function passed last_modified_func should return a standard datetime value specifying the last time the resource was modified, or None if the resource doesn't exist. The function passed to the etag decorator should return a string representing the ETag for the resource, or None if it doesn't exist.

如果它们没有通过视图设置并且请求的方法是安全的(GETHEAD),那么装饰器就会在请求上设置 ETagLast-Modified headers 。

用一个例子来解释如何有效地使用这个功能。假设你已经有了这些模型,代表一个博客系统:

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

如果在首页正在显示最新的博客文章,只会在有新博客文章时改变,你可以很快地计算最后修改时间。你需要与该博客关联的每篇文章的最新发布时间。一种方法是这样做的:

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):
    ...

小心装饰器的顺序

When condition() returns a conditional response, any decorators below it will be skipped and won't apply to the response. Therefore, any decorators that need to apply to both the regular view response and a conditional response must be above condition(). In particular, vary_on_cookie(), vary_on_headers(), and cache_control() should come first because RFC 9110 requires that the headers they set be present on 304 responses.

仅用于计算一个值的快捷方式

作为常用规则,如果你可以提供函数去同时计算 ETag 和最后的修改时间,你应该这样做。你不知道任何给定的 HTTP 客户端将向你发送哪一个headers,因此需要同时处理这两个headers。然而,有时候只有一个值易于计算,Django 提供的装饰器处理只有 ETag 或 只有 last-modified 的计算。

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 类似)。它也可以被用于提供 POST, PUTDELETE 请求的检查。在这些情况下,不会返回一个“未修改”的响应,但会告诉客户端它们尝试修改的资源在这期间已被修改。

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

  1. 用户请求``/foo/``
  2. 服务端用一些带有 "abcd1234" 的 ETag 响应一些内容。
  3. 客户端发送一个 HTTP PUT 请求到 /foo/ 来更新一些资源。它也发送 If-Match: "abcd1234" header 来指定它准备更新的版本。
  4. 服务端通过计算 ETag(与 GET 请求计算的方式相同,使用相同的函数)来检查资源是否被修改。如果资源被改变,它将返回 412 状态码,意思是 "先决条件失败" 。
  5. 客户端在收到一个412响应后会发送一个 GET 请求到 /foo/,在更新它之前用来寻找内容更新的版本。

重要的是这个例子显示的是相同函数在所有情形下可以被用来计算ETag和最后一次修改。事实上,你应该使用相同的函数,这样相同的值会被实时返回。

具有不安全请求方法的验证 headers

The condition decorator only sets validator headers (ETag and Last-Modified) for safe HTTP methods, i.e. GET and HEAD. If you wish to return them in other cases, set them in your view. See RFC 9110#section-9.3.4 to learn about the distinction between setting a validator header in response to requests made with PUT versus POST.

对比中间件的条件处理

Django 通过 django.middleware.http.ConditionalGetMiddleware 提供了有条件的 GET 处理。虽然易于使用,但是中间件在高级用法上是有限制的:

  • 它可被全局应用于项目的所有视图。
  • 它不会阻止你生成响应,这样代价可能很昂贵。
  • 它只适合HTTP 的 GET 请求。

你应该为你的特殊需求选择最合适的工具。如果你有方法可以很迅速地计算 ETags 和 修改时间,并且如果一些视图花了一些时间去生成内容,那么你应该在这个文档中考虑使用 condition 装饰器描述。如果所有事务都运行的非常快了,那么坚持使用中间件。如果视图没有变动,那么发送回客户端的网络流量将仍然会减少。