条件视图处理

HTTP 客户端可以发送许多headers来告诉服务器已经查看到资源的副本了。这通常用在检索网页时,避免发送所有那些已经被检索过的数据。但是,相同的headers可以被用来服务所有的HTTP方法 (POST, PUT, DELETE, 等等)

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

当客户端下一次请求相同资源时,它可能会发送header,如 If-modified-since 或 If-unmodified-since,包含它发送后最后一次修改时间,或者 If-match or If-none-match ,包含它发送的最后一个 ETag。如果页面的当前版本与客户端发送的 ETag 匹配,或者如果资源没有被修改,那么就会返回一个304状态,告诉客户端:“资源没有任何变化”,而不是返回所有资源响应。根据这个 header,如果页面发生了修改或者客户端没有匹配 ETag ,会返回 412 状态码。

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

条件装饰器

很多时候你可以创建函数去迅速计算 ETag 值或资源的上次修改时间, 需要进行构建所有视图的所有计算。然后 Django 可以使用函数为视图控制提供 "提前判断" 选项。或许告诉客户端自上次请求以来内容没有修改。

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

条件装饰器如下:

  1. condition(etag_func=None, last_modified_func=None)

为了计算ETag和最后修改时间,这两个函数将与它们协助包装视图函数时的相同顺序传递正在传入的 request 对象和相同的参数。函数传递 last_modified_func 应该返回一个标准时间的时间值,指定资源最后修改的时间,如果资源不存在,返回 None 。函数传递到 etag 装饰器应该返回表示资源的 ETag 字符串,如果不存在则返回 None

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

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

  1. import datetime
  2. from django.db import models
  3.  
  4. class Blog(models.Model):
  5. ...
  6.  
  7. class Entry(models.Model):
  8. blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
  9. published = models.DateTimeField(default=datetime.datetime.now)
  10. ...

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

  1. def latest_entry(request, blog_id):
  2. return Entry.objects.filter(blog=blog_id).latest("published").published

然后你可以使用这个函数预先为首页视图提供未变动页面的检测:

  1. from django.views.decorators.http import condition
  2.  
  3. @condition(last_modified_func=latest_entry)
  4. def front_page(request, blog_id):
  5. ...

小心装饰器的顺序

condition() 返回一个条件响应,在其下面的任何装饰器将被忽略并不会应用于响应。因此,任何需要同时应用于常规视图响应和条件响应的装饰器必须在 condition() 上面。特别是,vary_on_cookie(), vary_on_headers()cache_control() 应该首先出现,因为 RFC 7232 要求设置的 headers 需要出现在 304 响应中。

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

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

django.views.decorators.http.etagdjango.views.decorators.http.last_modified 装饰器和 condition 装饰器一样传递相同的函数类型。它们的签名是这样的:

  1. etag(etag_func)
  2. last_modified(last_modified_func)

我们可以使用其中一个装饰器来编写更早一些的那个只使用last-modified函数的例子:

  1. @last_modified(latest_entry)def front_page(request, blog_id):

…或:

  1. def front_page(request, blog_id):
  2. ...
  3. front_page = last_modified(latest_entry)(front_page)

测试两个条件时使用 condition

如果你想测试两个先决条件,那么试着链接 etaglast_modified 装饰器可能看起来更好。然而,这会导致错误的行为。

  1. # Bad code. Don't do this!
  2. @etag(etag_func)
  3. @last_modified(last_modified_func)
  4. def my_view(request):
  5. # ...
  6.  
  7. # End of bad code.

第一个装饰器不知道关于第二个装饰器的任何信息,而且可能回答“这个响应没有被修改”,即使第二个装饰器确定不是那样。condition 装饰器同时使用两个回调函数来执行正确的动作。

将装饰器和其他 HTTP 方法一起使用

condition 装饰器不仅仅用于 GETHEAD 请求(在这个解决方案里 HEAD请求和GET 类似)。它也可以被用于提供 POST, PUTDELETE 请求的检查。在这些情况下,不会返回一个“未修改”的响应,但会告诉客户端它们尝试修改的资源在这期间已被修改。

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

  • 用户请求/foo/[]($358b424d243f7dae.md#id1)。
  • 服务端用一些带有 "abcd1234" 的 ETag 响应一些内容。
  • 客户端发送一个 HTTP PUT 请求到 /foo/ 来更新一些资源。它也发送 If-Match: "abcd1234" header 来指定它准备更新的版本。
  • 服务端通过计算 ETag(与 GET 请求计算的方式相同,使用相同的函数)来检查资源是否被修改。如果资源被改变,它将返回 412 状态码,意思是 "先决条件失败" 。
  • 客户端在收到一个412响应后会发送一个 GET 请求到 /foo/,在更新它之前用来寻找内容更新的版本。重要的是这个例子显示的是相同函数在所有情形下可以被用来计算ETag和最后一次修改。事实上,你应该使用相同的函数,这样相同的值会被实时返回。

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

condition 装饰器只为安全的HTTP方法设置验证器headers(ETagLast-Modified)。如果你想在其他案例中返回它们,那就在你的视图中设置它们。查看 RFC 7231#section-4.3.4 来了解关于为响应 PUTPOST 发出的请求而设置验证器 header 之间的区别。

对比中间件的条件处理

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

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