条件查看处理

HTTP客户端可能发送一些首部来告诉服务端它们已经看过了哪些资源。 这在获取网页(使用HTTPGET请求)时非常常见,可以避免发送客户端已经获得的完整数据。 但是,相同的标题可以用于所有HTTP方法(POSTPUTDELETE等。)。

对于每一个Django从视图发回的页面(响应),都会提供两个HTTP协议头:Last-ModifiedETag 这些协议头在HTTP响应中是可选的。 您可以通过视图功能设置它们,或者您可以依赖ConditionalGetMiddleware中间件来设置ETag头。

当你的客户端再次请求相同的资源时,它可能会发送 If-modified-since 或者If-unmodified-since的协议头,包含之前发送的最后修改时间;或者 If-matchIf-none-match协议头,包含之前发送的ETag 如果页面的当前版本匹配客户端发送的ETag,或者如果资源没有被修改,会发回304状态码,而不是一个完整的回复,告诉客户端没有任何修改。 根据协议头,如果页面被修改了,或者不匹配客户端发送的 ETag,会返回412(先决条件失败,Precondition Failed)状态码。

当你需要更多精细化的控制时,你可以使用每个视图的按需处理函数。

condition装饰器

有时(实际上是经常),你可以创建一些函数来快速计算出资源的ETag值或者最后修改时间,并不需要执行构建完整视图所需的所有步骤。 Django可以使用这些函数来为视图处理提供一个“early bailout”的选项。 来告诉客户端,内容自从上次请求并没有任何改动。

这两个函数作为参数传递给django.views.decorators.http.condition装饰器。 这个装饰器使用这两个函数(如果你不能既快又容易得计算出来,你只需要提供一个)来弄清楚是否HTTP请求中的协议头匹配那些资源。 如果它们不匹配,会生成资源的一份新的副本,并调用你的普通视图。

condition装饰器的签名像这样:

condition(etag_func=None, last_modified_func=None)

计算ETag的最后修改时间的两个函数,会以相同的顺序传入request对象和相同的参数,就像它们封装的视图函数那样。 None函数应该返回一个标准的datetime值,它制订了资源修改的最后时间,或者资源不存在为 last_modified_func 传递给etag装饰器的函数应该返回一个表示资源的ETag的字符串,否则返回None(如果不存在)。

在Django更改1.11:

在旧版本中,来自etag_func()的返回值被解释为ETag的无引号部分。 这阻止了使用格式为W/"<string>"的弱ETag。 返回值现在预计是由规范(包括引号)定义的ETag,尽管未被引用的格式也被接受为向后兼容性。

用一个例子可以很好展示如何使用这一特性。 假设你有这两个模型,表示一个简单的博客系统:

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响应中。

仅计算一个值的快捷方式

一个普遍的原则是,如果你提供了计算 ETag最后修改时间的函数,你应该这样做: 你并不知道HTTP客户端会发给你哪个协议头,所以要准备好处理两种情况。 但是,有时只有二者之一容易计算,并且Django只提供给你计算ETag或最后修改日期的装饰器。

django.views.decorators.http.last_modifieddjango.views.decorators.http.etag作为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

如果你想要测试两个先决条件,把last_modifiedetag装饰器链到一起看起来很不错。 但是,这会导致不正确的行为:

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

# End of bad code.

第一个装饰器不知道后面的任何事情,并且可能发送“未修改”的响应,即使第二个装饰器会处理别的事情。 condition装饰器同时更使用两个回调函数,来弄清楚哪个是正确的行为。

使用其他HTTP方法的装饰器

GET装饰器不仅仅对conditionHEAD请求有用(HEAD请求在这种情况下和GET相同)。 它也可以用于为 PUT, POSTDELETE请求提供检查。 在这些情况下,主意是不要去返回一个“未修改(not modified,314)”的响应,而是要告诉服务端,它们尝试修改的资源在此期间被修改了。

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

  1. 客户端请求/foo/
  2. 服务端回复一些带有"abcd1234"ETag的内容。
  3. 客户端发送HTTP /foo/ 请求到 PUT 来更新资源。 它也发送一个 如果-比赛: “ABCD1234” 标题来指定它正在尝试更新的版本。
  4. 服务端检查是否资源已经被修改,通过和GET 上所做的相同方式计算ETag(使用相同的函数)。 如果资源更改了,它将返回412状态代码,意思是“前提条件失败”。
  5. 客户端在接收到412响应之后,发送 /foo/请求到 GET,来在更新之前获取内容的新版本。

重要的事情是,这个例子展示了在所有情况下,ETag和最后修改时间值都采用相同函数计算。 实际上,你 应该 使用相同函数,以便每次都返回相同的值。

与中间件条件处理进行比较

Django通过django.middleware.http.ConditionalGetMiddleware提供简单直接的条件GET处理。 虽然易于使用并适合于许多情况,但中间件对高级用途有限制:

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

在这里,你应该选择最适用于你特定问题的工具。 如果你有办法快速计算出ETag和修改时间,并且如果一些视图需要花一些时间来生成内容,你应该考虑使用这篇文档描述的condition装饰器。 如果一些都执行得非常快,坚持使用中间件在如果视图没有修改的条件下也会使发回客户端的网络流量也会减少。