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.