Django 6.1:条件视图处理

HTTP 客户端可以发送请求头,告诉服务器自己已经持有哪个版本的资源。这通常用于通过 HTTP GET 获取网页,避免重复传输已经获取过的内容;同样的请求头也可以用于 POST、PUT、DELETE 等其他 HTTP 方法。

Django 视图返回的页面可以提供两个可选的 HTTP 响应头:ETag 和 Last-Modified。可以在视图函数中设置它们,也可以依赖 ConditionalGetMiddleware 设置 ETag。

客户端再次请求同一资源时,可以发送 If-Modified-Since 或 If-Unmodified-Since,带上上次收到的最后修改时间;也可以发送 If-Match 或 If-None-Match,带上上次收到的 ETag。

如果页面当前版本与客户端的 ETag 匹配,或资源未被修改,服务器可以返回 304,告知客户端内容没有变化,而不必返回完整响应。根据使用的请求头,如果页面已被修改或不符合客户端的 ETag,则可能返回 412 Precondition Failed(先决条件失败)。需要更精细的控制时,可以为单个视图使用条件处理函数。

条件装饰器

很多时候,可以快速计算资源的 ETag 或最后修改时间,而不必执行构造完整视图所需的计算。Django 可以据此提前结束视图处理,例如直接告诉客户端内容自上次请求后没有变化。

把这两个函数作为参数传入 django.views.decorators.http.condition。如果不能快速计算两个值,只需提供其中一个。装饰器会利用回调检查请求头与资源是否匹配;需要返回资源的新版本时,才调用视图。

condition(etag_func=None, last_modified_func=None)

回调接收传入的 request,以及被装饰视图所接收的其余参数,顺序相同。last_modified_func 返回表示资源最后修改时间的标准日期时间值;资源不存在时返回 None。ETag 回调返回表示 ETag 的字符串;资源不存在时同样返回 None。

对于安全方法 GET 和 HEAD,如果视图尚未设置 ETag、Last-Modified,装饰器会把它们设置到响应中。

假设博客系统具有以下模型:

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

注意装饰器顺序

当 condition() 返回条件响应时,位于它下方的装饰器会被跳过。因此,需要同时作用于普通响应与条件响应的装饰器必须位于 condition() 上方。尤其是 vary_on_cookie()、vary_on_headers() 和 cache_control();RFC 9110 要求它们设置的相关响应头也出现在 304 响应中。

只计算一个值的快捷方式

通常,能同时提供 ETag 与最后修改时间回调时,应当同时提供。无法预先知道某个客户端会发送哪个条件请求头,因此最好兼顾两者。若只有一个值易于计算,可以使用只处理 ETag 或最后修改时间的装饰器。

django.views.decorators.http.etag 和 django.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

把 etag 与 last_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 不仅适用于 GET、HEAD;这里 HEAD 的处理与 GET 相似。它也可以检查 POST、PUT、DELETE。这些方法不会收到“未修改”响应,但可以获知准备修改的资源在此期间已经变化。

例如:

  1. 用户请求 /foo/。
  2. 服务器返回内容,并带上 ETag "abcd1234"。
  3. 客户端向 /foo/ 发送 PUT 更新资源,附带 If-Match: "abcd1234",指定准备更新的版本。
  4. 服务器使用与 GET 相同的函数计算 ETag,检查资源是否变化。如果已经变化,返回 412。
  5. 客户端收到 412 后,再用 GET /foo/ 获取最新版本,之后重新准备更新。

这个例子说明,可以且应该在所有情形下使用相同的 ETag 和最后修改时间计算函数,使相同版本的资源始终得到相同的验证值。

不安全方法的验证器响应头

condition 仅为安全方法 GET、HEAD 设置 ETag 与 Last-Modified。如果需要在其他方法的响应中返回它们,应在视图中设置。关于响应 PUT 与 POST 时设置验证器的区别,参见 RFC 9110 第 9.3.4 节。

与中间件条件处理的比较

django.middleware.http.ConditionalGetMiddleware 提供条件 GET 处理,容易使用,但有以下限制:

  • 它在项目层面应用于所有视图。
  • 它不会避免生成完整响应,响应生成本身仍可能很昂贵。
  • 它仅适用于 HTTP GET。

应根据需求选择。如果可以快速计算 ETag 和修改时间,而某些视图生成内容耗时较长,就考虑使用 condition。如果各项计算本来就很快,继续使用中间件即可;视图内容未变化时,仍能减少发送给客户端的网络流量。


来源:条件视图处理,Django 6.1 文档。本文整理已有中文并翻译残留英文,保留所有代码、HTTP 方法和状态码。

Copyright (c) Django Software Foundation and individual contributors. All rights reserved. 采用 BSD 三条款许可。

允许以源代码或二进制形式再分发和使用,无论是否修改,但须满足:源代码再分发保留上述版权声明、条件与下列免责声明;二进制再分发在文档和/或其他随附材料中重现上述版权声明、条件与下列免责声明;未经事先书面许可,不得使用 Django 或贡献者的名称为衍生产品背书或推广。

本软件由版权持有人与贡献者按原样提供,不作任何明示或默示保证,包括但不限于适销性及特定用途适用性。无论依据合同、严格责任或侵权(包括过失或其他原因),版权持有人与贡献者均不对因使用本软件产生的任何直接、间接、附带、特殊、惩罚性或后果性损失负责,包括替代商品或服务采购、使用损失、数据损失、利润损失或业务中断,即使已被告知可能发生此类损失。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容