JavaScript, fetch и JSON

Вы можете захотеть сделать HTML-страницу динамической, изменяя данные без перезагрузки всей страницы. Вместо того чтобы отправлять HTML <form> и выполнять перенаправление для повторного отображения шаблона, вы можете добавить JavaScript, который вызывает fetch() и заменяет содержимое страницы.

fetch() - это современное встроенное решение JavaScript для выполнения запросов со страницы. Возможно, вы слышали о других методах и библиотеках «AJAX», таких как XMLHttpRequest() или jQuery. Они больше не нужны в современных браузерах, хотя вы можете использовать их или другие библиотеки в зависимости от требований вашего приложения. В этой документации речь пойдет только о встроенных функциях JavaScript.

Шаблоны рендеринга

Важно понимать разницу между шаблонами и JavaScript. Шаблоны отображаются на сервере перед отправкой ответа в браузер пользователя. JavaScript выполняется в браузере пользователя после того, как шаблон отрисован и отправлен. Поэтому невозможно использовать JavaScript, чтобы повлиять на то, как отображается шаблон Jinja, но можно передать данные в JavaScript, который будет запущен.

Чтобы предоставить данные JavaScript при рендеринге шаблона, используйте фильтр tojson() в блоке <script>. Это преобразует данные в правильный объект 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>

Менее распространенная схема - добавление данных в атрибут data- в HTML-теге. В этом случае вы должны использовать одинарные кавычки вокруг значения, а не двойные кавычки, иначе вы получите недопустимый или небезопасный 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(...)

Однако вам может понадобиться сгенерировать URL на основе информации, известной только в JavaScript. Как говорилось выше, JavaScript выполняется в браузере пользователя, а не в процессе рендеринга шаблона, поэтому в этом случае нельзя использовать url_for.

В этом случае вам нужно знать «корневой URL», под которым обслуживается ваше приложение. В простых установках это /, но это может быть и что-то другое, например https://example.com/myapp/.

Простой способ сообщить коду JavaScript об этом корне - установить его в качестве глобальной переменной при рендеринге шаблона. Затем вы можете использовать ее при генерации URL из JavaScript.

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. Мы не будем рассматривать все доступные опции, и будем использовать только 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-форма, и доступ к нему осуществляется с помощью request.form в представлении Flask.

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, либо новый раздел страницы для добавления или замены, либо полностью новая страница. В общем случае, если вы возвращаете всю страницу, то лучше сделать это с помощью перенаправления, как показано в предыдущем разделе. В следующем примере показано, как заменить <div> на HTML, возвращенный запросом.

<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 из представлений

Чтобы вернуть объект JSON из представления API, вы можете напрямую вернуть 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()
Вернуться на верх