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

从服务器获取数据的另一种方法是请求数据。首先,您需要知道要请求的 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()