條件式視圖處理

HTTP 客戶端可以傳送許多標頭來告知伺服器他們已經看過資源的副本。這通常用於檢索網頁 (使用 HTTP GET 請求) 以避免傳送客戶端已經檢索的內容的所有資料。但是,相同的標頭可以用於所有 HTTP 方法 (POSTPUTDELETE 等)。

對於 Django 從視圖傳回的每個頁面(回應),它可能會提供兩個 HTTP 標頭:ETag 標頭和 Last-Modified 標頭。這些標頭在 HTTP 回應中是選用的。它們可以由您的視圖函式設定,或者您可以依靠 ConditionalGetMiddleware 中介軟體來設定 ETag 標頭。

當客戶端下次請求相同的資源時,它可能會傳送一個標頭,例如 If-Modified-SinceIf-Unmodified-Since,其中包含上次傳送的最後修改時間,或者 If-MatchIf-None-Match,其中包含上次傳送的 ETag。如果頁面的目前版本與客戶端傳送的 ETag 相符,或者如果資源未被修改,則可以傳回 304 狀態碼,而不是完整的回應,告知客戶端沒有任何變更。根據標頭,如果頁面已被修改或與客戶端傳送的 ETag 不符,則可能會傳回 412 狀態碼 (先決條件失敗)。

當您需要更細緻的控制時,您可以使用每個視圖的條件式處理函式。

condition 裝飾器

有時(事實上,很常見),您可以建立函式來快速計算資源的 ETag 值或上次修改時間,而無需執行建構完整視圖所需的所有計算。然後,Django 可以使用這些函式為視圖處理提供「提早中止」選項。例如,告知客戶端內容自上次請求以來未被修改。

這兩個函式會以參數的形式傳遞給 django.views.decorators.http.condition 裝飾器。此裝飾器使用這兩個函式(如果無法輕鬆快速地計算兩個量,您只需要提供一個函式)來判斷 HTTP 請求中的標頭是否與資源上的標頭相符。如果它們不相符,則必須計算資源的新副本,並呼叫您的正常視圖。

condition 裝飾器的簽名如下

condition(etag_func=None, last_modified_func=None)

這兩個函式(用於計算 ETag 和上次修改時間)將會傳遞傳入的 request 物件,以及與它們正在協助包裝的視圖函式相同的參數(以相同的順序)。傳遞給 last_modified_func 的函式應該傳回一個標準的日期時間值,指定資源上次修改的時間,如果資源不存在,則傳回 None。傳遞給 etag 裝飾器的函式應該傳回一個字串,代表資源的 ETag,如果資源不存在,則傳回 None

如果視圖尚未設定,且請求方法是安全的 (GETHEAD),則裝飾器會在回應中設定 ETagLast-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)
    ...

如果顯示最新部落格文章的首頁只有在您新增新的部落格文章時才會變更,則您可以非常快速地計算上次修改時間。您需要與該部落格相關聯的每篇文章的最新 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 9110 要求它們設定的標頭必須出現在 304 回應中。

僅計算一個值的快捷方式

一般而言,如果您可以提供函式來計算 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. 伺服器以 ETag 為 "abcd1234" 的一些內容回應。

  3. 客戶端將 HTTP PUT 請求傳送至 /foo/ 以更新資源。它也會傳送 If-Match: "abcd1234" 標頭來指定它嘗試更新的版本。

  4. 伺服器會檢查資源是否已變更,方法是使用與 GET 請求相同的方式(使用相同的函式)來計算 ETag。如果資源變更,它會傳回 412 狀態碼,表示「先決條件失敗」。

  5. 客戶端會在收到 412 回應後,傳送 GET 請求至 /foo/,以檢索內容的更新版本,然後再更新它。

這個範例顯示的重要一點是,相同的函式可以用於在所有情況下計算 ETag 和上次修改值。事實上,您應該使用相同的函式,以便每次都傳回相同的值。

具有非安全請求方法的驗證器標頭

condition 裝飾器僅為安全的 HTTP 方法(即 GETHEAD)設定驗證器標頭 (ETagLast-Modified)。如果您希望在其他情況下傳回它們,請在您的視圖中設定它們。請參閱 RFC 9110 第 9.3.4 節,以了解在回應以 PUTPOST 發出的請求時設定驗證器標頭之間的區別。

與中介軟體條件式處理的比較

Django 透過 django.middleware.http.ConditionalGetMiddleware 提供條件式 GET 處理。雖然適用於許多情況,但中介軟體對於進階使用有其限制。

  • 它會全域應用於專案中的所有檢視。

  • 它並不會讓您免於產生回應,而產生回應的過程可能會很耗費效能。

  • 它僅適用於 HTTP GET 請求。

您應該在此選擇最適合您特定問題的工具。如果您有方法可以快速計算 ETags 和修改時間,而且某些檢視需要一段時間才能產生內容,則應考慮使用本文檔中描述的 condition 裝飾器。如果一切都已經執行得相當快,請堅持使用中介軟體,如果檢視沒有變更,仍可減少傳回用戶端的網路流量。

回到頂部