JavaScript、fetch
和 JSON¶
您可能希望讓您的 HTML 頁面具有動態效果,透過更改資料而無需重新載入整個頁面。您可以新增 JavaScript,呼叫 fetch()
並替換頁面上的內容,而不是提交 HTML <form>
並執行重新導向以重新渲染範本。
fetch()
是現代、內建的 JavaScript 解決方案,用於從頁面發出請求。您可能聽說過其他「AJAX」方法和函式庫,例如 XMLHttpRequest()
或 jQuery。在現代瀏覽器中,這些已不再需要,儘管您可能會根據應用程式的需求選擇使用它們或其他函式庫。這些文件將僅關注內建的 JavaScript 功能。
渲染範本¶
重要的是要理解範本和 JavaScript 之間的區別。範本是在伺服器上渲染的,在回應發送到使用者瀏覽器之前。JavaScript 在使用者瀏覽器中執行,在範本渲染並發送之後。因此,不可能使用 JavaScript 來影響 Jinja 範本的渲染方式,但可以將資料渲染到將要執行的 JavaScript 中。
為了在渲染範本時向 JavaScript 提供資料,請在 <script>
區塊中使用 tojson()
過濾器。這會將資料轉換為有效的 JavaScript 物件,並確保安全地渲染任何不安全的 HTML 字元。如果您不使用 tojson
過濾器,您將在瀏覽器主控台中收到 SyntaxError
。
data = generate_report()
return render_template("report.html", chart_data=data)
<script>
const chart_data = {{ chart_data|tojson }}
chartLib.makeChart(chart_data)
</script>
一個較不常見的模式是在 HTML 標籤上將資料新增到 data-
屬性。在這種情況下,您必須在值周圍使用單引號,而不是雙引號,否則您將產生無效或不安全的 HTML。
<div data-chart='{{ chart_data|tojson }}'></div>
產生 URL¶
從伺服器取得資料到 JavaScript 的另一種方法是發出請求來取得它。首先,您需要知道要請求的 URL。
產生 URL 最簡單的方法是繼續在渲染範本時使用 url_for()
。例如
const user_url = {{ url_for("user", id=current_user.id)|tojson }}
fetch(user_url).then(...)
但是,您可能需要根據您僅在 JavaScript 中知道的資訊來產生 URL。如上所述,JavaScript 在使用者的瀏覽器中執行,而不是作為範本渲染的一部分,因此您無法在該點使用 url_for
。
在這種情況下,您需要知道您的應用程式所服務的「根 URL」。在簡單的設定中,這是 /
,但也可能是其他東西,例如 https://example.com/myapp/
。
告訴您的 JavaScript 程式碼關於這個根 URL 的一個簡單方法是在渲染範本時將其設定為全域變數。然後您可以在從 JavaScript 產生 URL 時使用它。
const SCRIPT_ROOT = {{ request.script_root|tojson }}
let user_id = ... // do something to get a user id from the page
let user_url = `${SCRIPT_ROOT}/user/${user_id}`
fetch(user_url).then(...)
使用 fetch
發出請求¶
fetch()
接受兩個引數,一個 URL 和一個包含其他選項的物件,並返回一個 Promise
。我們不會涵蓋所有可用的選項,並且只會在 promise 上使用 then()
,而不是其他回呼或 await
語法。請閱讀連結的 MDN 文件以取得關於這些功能的更多資訊。
預設情況下,使用 GET 方法。如果回應包含 JSON,則可以與 then()
回呼鏈一起使用。
const room_url = {{ url_for("room_detail", id=room.id)|tojson }}
fetch(room_url)
.then(response => response.json())
.then(data => {
// data is a parsed JSON object
})
要發送資料,請使用資料方法(例如 POST),並傳遞 body
選項。資料最常見的類型是表單資料或 JSON 資料。
要發送表單資料,請傳遞一個已填寫的 FormData
物件。這使用與 HTML 表單相同的格式,並且可以在 Flask 視圖中使用 request.form
存取。
let data = new FormData()
data.append("name", "Flask Room")
data.append("description", "Talk about Flask here.")
fetch(room_url, {
"method": "POST",
"body": data,
}).then(...)
一般而言,建議將請求資料作為表單資料發送,就像提交 HTML 表單時一樣。JSON 可以表示更複雜的資料,但除非您需要這樣做,否則最好堅持使用更簡單的格式。發送 JSON 資料時,也必須發送 Content-Type: application/json
標頭,否則 Flask 將返回 400 錯誤。
let data = {
"name": "Flask Room",
"description": "Talk about Flask here.",
}
fetch(room_url, {
"method": "POST",
"headers": {"Content-Type": "application/json"},
"body": JSON.stringify(data),
}).then(...)
追蹤重新導向¶
回應可能是重新導向,例如,如果您使用 JavaScript 而不是傳統的 HTML 表單登入,並且您的視圖返回重新導向而不是 JSON。JavaScript 請求確實會追蹤重新導向,但它們不會更改頁面。如果您想讓頁面更改,您可以檢查回應並手動應用重新導向。
fetch("/login", {"body": ...}).then(
response => {
if (response.redirected) {
window.location = response.url
} else {
showLoginError()
}
}
)
替換內容¶
回應可能是新的 HTML,可以是新增或替換的頁面新區段,或完全是新的頁面。一般而言,如果您要返回整個頁面,最好使用上一節中所示的重新導向來處理。以下範例展示如何使用請求返回的 HTML 替換 <div>
。
<div id="geology-fact">
{{ include "geology_fact.html" }}
</div>
<script>
const geology_url = {{ url_for("geology_fact")|tojson }}
const geology_div = getElementById("geology-fact")
fetch(geology_url)
.then(response => response.text)
.then(text => geology_div.innerHTML = text)
</script>
從視圖返回 JSON¶
要從您的 API 視圖返回 JSON 物件,您可以直接從視圖返回一個 dict。它將自動序列化為 JSON。
@app.route("/user/<int:id>")
def user_detail(id):
user = User.query.get_or_404(id)
return {
"username": User.username,
"email": User.email,
"picture": url_for("static", filename=f"users/{id}/profile.png"),
}
如果您想返回另一個 JSON 類型,請使用 jsonify()
函式,它會建立一個回應物件,其中包含序列化為 JSON 的給定資料。
from flask import jsonify
@app.route("/users")
def user_list():
users = User.query.order_by(User.name).all()
return jsonify([u.to_json() for u in users])
在 JSON 回應中返回檔案資料通常不是一個好主意。JSON 無法直接表示二進制資料,因此必須進行 base64 編碼,這可能會很慢、佔用更多頻寬來發送,並且不容易快取。相反,請使用一個視圖來服務檔案,並產生一個指向所需檔案的 URL 以包含在 JSON 中。然後,用戶端可以在取得 JSON 後發出單獨的請求來取得連結的資源。
在視圖中接收 JSON¶
使用 json
物件的 request
屬性,將請求的主體解碼為 JSON。如果主體不是有效的 JSON,或者 Content-Type
標頭未設定為 application/json
,則會引發 400 Bad Request 錯誤。
from flask import request
@app.post("/user/<int:id>")
def user_update(id):
user = User.query.get_or_404(id)
user.update_from_json(request.json)
db.session.commit()
return user.to_json()