2025, Oct 16 09:19

Как обновлять часть страницы во Flask через fetch и JSON

Как во Flask сохранять данные в sqlite3 и обновлять нужный блок без перезагрузки страницы: fetch, JSON-ответы, пример, советы по работе с @login_required.

Когда представление Flask рендерит шаблон, зависящий от состояния базы данных, самый простой способ отразить изменения после записи — полная перезагрузка страницы. Это работает, но выглядит резко и неэффективно, особенно если нужно обновить только один фрагмент — например, список классов. Задача — сохранить новые данные в sqlite3 и обновить лишь часть страницы без полного перезапуска.

Постановка задачи

Вот структура базы данных, которая участвует в этом кейсе:

CREATE TABLE classes (
    model_id TEXT NOT NULL,
    class_number INTEGER,
    class_name TEXT,
    FOREIGN KEY(model_id) REFERENCES models(model_id)
);

Страница, показывающая одну модель, отдается из такого маршрута. Шаблон получает несколько переменных, в том числе список классов и изображения, которые формируются во вьюхе:

@app.route("/model/<model_id>", methods=["POST", "GET"])
@login_required
def show_model(model_id):
    # ... логика приложения опущена ...
    with sqlite3.connect("database.db") as conn:
        # ... запросы, которые заполняют переменные ниже ...
        return render_template(
            "train.html",
            categories=ALL_GROUPS,
            model_id=model_id,
            model=mdl,
            classes=class_rows,
            images=pics,
            istrained=trained_flag,
            test_image=probe_img,
        )

Чтобы создать новый класс, отдельный маршрут записывает данные в базу, а затем делает редирект обратно на страницу модели. Редирект заставляет перезагрузить страницу целиком, чтобы подтянуть обновления.

@app.route("/add_class/<model_id>", methods=["POST", "GET"])
@login_required
def create_class(model_id):
    with sqlite3.connect("database.db") as conn:
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()
        row = cur.execute(
            "SELECT MAX(class_number) FROM classes WHERE model_id = ?", (model_id,)
        ).fetchone()
        max_no = row[0] if row and row[0] is not None else 0
        next_no = int(max_no + 1)
        next_name = "Class " + str(max_no + 1)
        cur.execute(
            "INSERT INTO classes (model_id, class_name, class_number) VALUES (?, ?, ?)",
            (model_id, next_name, next_no),
        )
        conn.commit()
    return redirect(f"/model/{model_id}")

Возврат редиректа перезагружает всю страницу — это работает, но UX получается не тем, что хочется. Пустой ответ или другой статус-код не помогут, потому что подготовка данных для страницы происходит в первом маршруте.

В чем корень проблемы

Flask рендерит шаблоны на сервере и отдает целиком готовый HTML. Встроенного механизма, который сам по себе отправляет в браузер частичные обновления, у него нет. Без запроса со стороны клиента сервер не может обновить только фрагмент страницы. Поэтому редирект заново запускает рендеринг и перезагружает все, а ответ без редиректа не заставит раздел с классами обновиться «сам собой».

В Flask нет специальных функций, чтобы отправлять данные из браузера на сервер без перезагрузки страницы.

Чтобы менять содержимое без полной перезагрузки, браузер должен асинхронно обратиться к серверу и обновить DOM на месте. Для этого нужен JavaScript. Практичный путь — использовать стандартный fetch() для отправки запроса во Flask и получения данных (например, JSON), которые затем применяются к странице. Flask, в свою очередь, может проверить request.method и request.is_json и ответить через jsonify(...), чтобы браузер смог распарсить и использовать результат. Если представление защищено @login_required, запрос fetch должен включать учетные данные, чтобы передать куки.

Решение: асинхронный вызов через fetch() и ответ в JSON

Пример ниже показывает, как отправить запрос со страницы во Flask без перезагрузки, получить JSON‑статус и вывести его на месте. Также видно, как это работает вместе с @login_required — добавляется credentials: 'include' в fetch().

from flask import Flask, request, jsonify, render_template_string, redirect
from flask_login import LoginManager, UserMixin, login_required, login_user, logout_user
class Account(UserMixin):
    def __init__(self, uid, mail, secret):
        self.id = uid
        self.email = mail
        self.password = secret
# ключ совпадает с Account.id
account_store = {
    '007': Account('007', 'james_bond@mi6.gov.uk', 'license_to_kill')
}
srv = Flask(__name__)
srv.secret_key = "super secret string"  # замените в реальном приложении
auth = LoginManager()
auth.init_app(srv)
@auth.user_loader
def load_account(user_id):
    print('load_account:', user_id, account_store.get(user_id))
    return account_store.get(user_id)
@srv.route('/login')
def do_login():
    user = account_store.get('007')
    print('login:', user)
    login_user(user)
    print('redirect:', '/')
    return redirect('/')
@srv.route('/logout')
@login_required
def do_logout():
    logout_user()
    return redirect('/')
@srv.route('/')
def home():
    return render_template_string("""
<script type="text/javascript">
function pushData(modelId, httpMethod) {
    fetch("/add_model/" + modelId, {
        method: httpMethod,  // "POST" или "GET"
        // body: JSON.stringify({"username": "example"}), // если нужно отправить JSON в теле
        headers: {
            // "Content-Type": "application/x-www-form-urlencoded", // для <form>
            "Content-Type": "application/json", // включает request.is_json в Flask
            // "X-Requested-With": "XMLHttpRequest" // типичный заголовок AJAX
        },
        credentials: 'include', // куки для @login_required
    }).then((resp) => {
        if (!resp.ok) {
            throw new Error(`HTTP error! Status: ${resp.status}`);
        }
        return resp.json(); // ожидаем JSON
        // return resp.text(); // если Flask возвращает text/HTML
    }).then((payload) => {
        document.getElementById("statusBox").innerHTML = `model #${modelId}: ${payload['status']}`;
    }).catch((err) => {
        console.log('Fetch problem: ' + err.message);
        document.getElementById("statusBox").innerHTML = 'Fetch problem: ' + err.message;
    });
}
</script>
{% if current_user.is_authenticated %}
<p>Logged in as: {{ current_user.id }} <a href="{{ url_for('do_logout') }}">Logout</a></p>
{% else %}
<p>You are not logged in. <a href="{{ url_for('do_login') }}">Login</a></p>
{% endif %}
{% for mid in range(1, 6) %}
<button onclick="pushData({{ mid }}, 'POST')">Add model #{{ mid }} (POST)</button>
<button onclick="pushData({{ mid }}, 'GET')">Add model #{{ mid }} (GET)</button>
<br/>
{% endfor %}
<div id="statusBox"></div>
""")
@srv.route('/add_model/<model_id>', methods=['GET', 'POST'])
@login_required
def add_model(model_id):
    print(f'add_model: {model_id} | method: {request.method} | is_json: {request.is_json}')
    if request.method == 'POST' and request.is_json:
        return jsonify({'status': 'OK'})
    else:
        return jsonify({'status': f'Wrong Method {request.method}'})
if __name__ == "__main__":
    srv.run(host='0.0.0.0')

Именно такой шаблон лежит в основе частичных обновлений без полной перезагрузки. Браузер обращается к маршруту Flask через fetch(). Flask возвращает JSON с помощью jsonify(...). Страница читает этот JSON и меняет только нужный участок DOM. Если доступ требует авторизации, в запрос добавляются credentials, чтобы сервер проверил сессию для @login_required.

Почему это важно

Когда страница зависит от данных, собранных в одном маршруте, редирект гарантирует актуальность, но ценой повторного рендеринга всего. Асинхронные вызовы сохраняют серверную логику прежней и при этом дают более плавный интерфейс. Важный момент: request.method и request.is_json позволяют серверу различать типы запросов, а jsonify — корректно форматировать ответ, который удобно обрабатывать в браузере.

Практический итог

Чтобы менять содержимое без перезагрузки всей страницы, инициируйте асинхронный запрос из браузера. Используйте fetch(), вызывайте маршрут Flask, возвращайте JSON (или HTML, если хотите вставлять его напрямую) и обновляйте страницу на месте. Если маршруты защищены @login_required, указывайте credentials в fetch(). Это прямой и надежный способ получить частичные обновления в приложении Flask, которое опирается на состояние в sqlite3.

Статья основана на вопросе с StackOverflow от Burak и ответе от furas.