Создайте многостраничное приложение Reflex с реальным временем и реактивным UI на Python
Пошаговый туториал по созданию доски заметок на Reflex с реальной базой, фильтрами и аналитикой.
Зачем Reflex и что мы собираем
В этом руководстве показано, как создать продвинутое веб-приложение на Reflex полностью на Python и запустить его в Google Colab. Проект демонстрирует, как Reflex позволяет делать full-stack разработку без JavaScript, используя реактивный Python для фронтенда и бэкенда. Итоговый проект — многостраничная доска заметок с постоянным SQLite-хранилищем, живыми обновлениями, фильтрацией, сортировкой, аналитикой и персонализацией пользователя.
Настройка в Colab
Создайте папку проекта и установите Reflex. Этот фрагмент кода подготавливает окружение и устанавливает конкретную версию Reflex:
import os, subprocess, sys, pathlib
APP = "reflex_colab_advanced"
os.makedirs(APP, exist_ok=True)
os.chdir(APP)
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "reflex==0.5.9"])Он создаёт директорию проекта, переходит в неё и устанавливает Reflex, чтобы всё могло корректно работать в Colab.
Конфигурация
Опишите минимальный файл конфигурации, который задаёт имя приложения и соединение с базой данных. Пример записывает rxconfig.py с подключением к локальному SQLite:
rxconfig = """
import reflex as rx
class Config(rx.Config):
app_name = "reflex_colab_advanced"
db_url = "sqlite:///reflex.db"
config = Config()
"""
pathlib.Path("rxconfig.py").write_text(rxconfig)Минимальная конфигурация упрощает работу и сохраняет постоянные данные заметок.
Модель данных и реактивное состояние
Определите модель Note и реактивный класс State, который управляет вводом пользователя, фильтрами, сортировкой и операциями с базой. State содержит асинхронные методы для CRUD и вычисление статистики.
app_py = """
import reflex as rx
class Note(rx.Model, table=True):
content: str
tag: str = "general"
done: bool = False
class State(rx.State):
user: str = ""
search: str = ""
tag_filter: str = "all"
sort_desc: bool = True
new_content: str = ""
new_tag: str = "general"
toast_msg: str = ""
def set_user(self, v: str): self.user = v
def set_search(self, v: str): self.search = v
def set_tag_filter(self, v: str): self.tag_filter = v
def set_new_content(self, v: str): self.new_content = v
def set_new_tag(self, v: str): self.new_tag = v
def toggle_sort(self): self.sort_desc = not self.sort_desc
async def add_note(self):
if self.new_content.strip():
await Note.create(content=self.new_content.strip(), tag=self.new_tag.strip() or "general")
self.new_content = ""; self.toast_msg = "Note added"
async def toggle_done(self, note_id: int):
note = await Note.get(id=note_id)
if note:
await note.update(done=not note.done)
async def delete_note(self, note_id: int):
await Note.delete(id=note_id)
self.toast_msg = "Deleted"
async def clear_done(self):
items = await Note.all()
for n in items:
if n.done:
await Note.delete(id=n.id)
self.toast_msg = "Cleared done notes"
async def notes_filtered(self):
items = await Note.all()
q = self.search.lower()
if q:
items = [n for n in items if q in n.content.lower() or q in n.tag.lower()]
if self.tag_filter != "all":
items = [n for n in items if n.tag == self.tag_filter]
items.sort(key=lambda n: n.id, reverse=self.sort_desc)
return items
async def stats(self):
items = await Note.all()
total = len(items)
done = len([n for n in items if n.done])
tags = {}
for n in items:
tags[n.tag] = tags.get(n.tag, 0) + 1
top_tags = sorted(tags.items(), key=lambda x: x[1], reverse=True)[:5]
return {"total": total, "done": done, "pending": total - done, "tags": top_tags}
"""Этот код реализует асинхронные операции, логику фильтрации и подсчёта статистики, которые автоматически отражаются в UI.
UI-компоненты и компоновка
Создайте переиспользуемые компоненты: боковую панель, карточки статистики, фильтры по тегам, список заметок и строку заметки. Каждый компонент привязан к реактивному состоянию.
app_py += """
def sidebar():
return rx.vstack(
rx.heading("RC Advanced", size="6"),
rx.link("Dashboard", href="/"),
rx.link("Notes Board", href="/board"),
rx.text("User"),
rx.input(placeholder="your name", value=State.user, on_change=State.set_user),
spacing="3", width="15rem", padding="1rem", border_right="1px solid #eee"
)
async def stats_cards():
s = await State.stats()
return rx.hstack(
rx.box(rx.text("Total"), rx.heading(str(s["total"]), size="5"), padding="1rem", border="1px solid #eee", border_radius="0.5rem"),
rx.box(rx.text("Done"), rx.heading(str(s["done"]), size="5"), padding="1rem", border="1px solid #eee", border_radius="0.5rem"),
rx.box(rx.text("Pending"), rx.heading(str(s["pending"]), size="5"), padding="1rem", border="1px solid #eee", border_radius="0.5rem"),
spacing="4"
)
def tag_pill(tag: str, count: int = 0):
return rx.badge(
f"{tag} ({count})" if count else tag,
on_click=State.set_tag_filter(tag),
cursor="pointer",
color_scheme="blue" if tag == State.tag_filter else "gray"
)
async def tags_bar():
s = await State.stats()
tags = [("all", s["total"])] + s["tags"]
return rx.hstack(*[tag_pill(t[0], t[1]) for t in tags], spacing="2", wrap="wrap")
def note_row(note: Note):
return rx.hstack(
rx.hstack(
rx.checkbox(is_checked=note.done, on_change=State.toggle_done(note.id)),
rx.text(note.content, text_decoration="line-through" if note.done else "none"),
),
rx.badge(note.tag, color_scheme="green"),
rx.button("", on_click=State.delete_note(note.id), color_scheme="red", size="1"),
justify="between", width="100%"
)
async def notes_list():
items = await State.notes_filtered()
return rx.vstack(*[note_row(n) for n in items], spacing="2", width="100%")
"""Такой подход упрощает поддержку и расширение интерфейса.
Страницы, маршруты и запуск
Соберите страницы Dashboard и Notes Board, зарегистрируйте маршруты, скомпилируйте и запустите бэкенд:
app_py += """
def dashboard_page():
return rx.hstack(
sidebar(),
rx.box(
rx.heading("Dashboard", size="8"),
rx.cond(State.user != "", rx.text(f"Hi {State.user}, here is your activity")),
rx.vstack(
rx.suspense(stats_cards, fallback=rx.text("Loading stats...")),
rx.suspense(tags_bar, fallback=rx.text("Loading tags...")),
spacing="4"
),
padding="2rem", width="100%"
),
width="100%"
)
def board_page():
return rx.hstack(
sidebar(),
rx.box(
rx.heading("Notes Board", size="8"),
rx.hstack(
rx.input(placeholder="search...", value=State.search, on_change=State.set_search, width="50%"),
rx.button("Toggle sort", on_click=State.toggle_sort),
rx.button("Clear done", on_click=State.clear_done, color_scheme="red"),
spacing="2"
),
rx.hstack(
rx.input(placeholder="note content", value=State.new_content, on_change=State.set_new_content, width="60%"),
rx.input(placeholder="tag", value=State.new_tag, on_change=State.set_new_tag, width="20%"),
rx.button("Add", on_click=State.add_note),
spacing="2"
),
rx.cond(State.toast_msg != "", rx.callout(State.toast_msg, icon="info")),
rx.suspense(notes_list, fallback=rx.text("Loading notes...")),
padding="2rem", width="100%"
),
width="100%"
)
app = rx.App()
app.add_page(dashboard_page, route="/", title="RC Dashboard")
app.add_page(board_page, route="/board", title="Notes Board")
app.compile()
"""
pathlib.Path("app.py").write_text(app_py)
subprocess.run(["reflex", "run", "--env", "prod", "--backend-only"], check=False)Эта последовательность компилирует приложение Reflex, добавляет страницы и запускает бэкенд, делая интерфейс интерактивным.
Что вы получите и как развивать
- Полностью определённый на Python фронтенд и бэкенд с реактивностью Reflex.
- Постоянное хранение данных через SQLite и модельные CRUD-операции.
- Реактивную фильтрацию, сортировку, теговые группы и аналитические карточки.
- Чистую структуру для расширения: можно добавить аутентификацию, внешнюю БД или деплой на сервер.
Код и структура удобны для прототипирования и изучения концепции реактивных full-stack приложений на Python.
Switch Language
Read this article in English