[В этом руководстве объясняется, как создать приложение с помощью Flask и HTMx. Если вы хотите разместить приложение на HTMx и Flask на нашем PaaS, вы можете найти здесь краткое руководство по развертыванию, в котором используется тот же проект].
В последнее время в современном вебе одностраничные фреймворки, такие как React.js и Angular, заняли место традиционных многостраничных сайтов, в основном из-за недостатка интерактивности, предлагаемой HTML. Однако стоит отметить, что одностраничные приложения (SPA) принесли эту интерактивность ценой дополнительной сложности.
Именно здесь вступает в игру новое расширение HTML под названием HTMx. HTMx придает традиционным HTML-сайтам большую интерактивность, сохраняя при этом простоту, поскольку позволяет делать запросы из любого элемента HTML, а не только из тегов <a>
и <form>
. Но это не единственное преимущество HTMx. К другим преимуществам относятся:
- Возможность выполнять частичную перезагрузку страницы в HTML
- Поддержка методов PUT и DELETE в дополнение к GET и POST
- Не ограничиваться только триггерами событий click и submit
- Легкая настройка — для работы не нужно устанавливать дополнительные зависимости.
В этом руководстве мы изучим преимущества HTMx, создав приложение полного стека с использованием Flask и HTMx. Наше приложение будет представлять собой приложение для рекомендаций книг, поддерживающее функциональность CRUD. Окончательный вариант приложения будет выглядеть примерно так:
Обзор и требования
После создания нашего приложения вы захотите развернуть его в продакшн, чтобы показать его друзьям и родственникам. Поэтому для выполнения этого урока вам понадобится следующее:
- Настроенный и установленный Git, а также зарегистрированный аккаунт на GitHub
- установленный Python 3
- IDE или текстовый редактор по вашему выбору
Настройка проекта
Когда все требования соблюдены, мы можем приступить к созданию нашего проекта. Давайте начнем с создания папки проекта, а затем создадим виртуальное окружение в папке проекта.
Создание папки проекта
Давайте создадим папку для размещения исходного кода нашего приложения. Выполните приведенные ниже команды в терминале, чтобы создать папку и перейти в нее.
mkdir flask-htmx
cd flask-htmx
С этого момента каталог flask-htmx
будет называться корневой папкой проекта.
Создание виртуальной среды
Виртуальная среда позволяет изолировать пакеты, необходимые для разработки приложений Python, от вашей компьютерной системы. Мы рекомендуем использовать новую виртуальную среду для каждого разрабатываемого приложения, чтобы не испортить зависимости для других приложений.
В терминале выполните следующую команду для создания виртуальной среды в корневой папке проекта:
python3 -m venv env
Чтобы активировать виртуальную среду, введите одну из следующих команд:
MacOS
source env/bin/activate
Windows
.envScriptsactivate
После активации виртуальной среды в левой части вашего терминала в текущей строке должно появиться имя env
в скобках. Это сигнализирует о том, что активация прошла успешно.
Установка зависимостей
Теперь мы можем установить наши необходимые пакеты в виртуальную среду, которую мы активировали в предыдущем шаге. Выполните приведенную ниже команду:
pip3 install flask flask-sqlalchemy gunicorn
Вы можете заметить, что в нашем списке зависимостей нет зависимости для HTMx. Это потому, что он будет добавлен в качестве тега сценария в шапку наших HTML-шаблонов.
Инициализация пустого Git-репозитория
Находясь в корневой папке проекта, введите команду git init
, чтобы инициализировать репозиторий git
. Это позволит вам отслеживать изменения в вашем приложении по мере его создания.
Создайте файл .gitignore
и добавьте в него строку ниже:
env/
Это исключит папку env
из отслеживания Git’ом, так как мы хотим отслеживать изменения только в файлах нашего проекта.
Ссылка на GitHub
Перейдите на GitHub и создайте новый репозиторий. Затем в корневой папке проекта выполните в терминале приведенную ниже команду, заменив username
и repository_name
своими значениями из GitHub.
git remote add origin git@github.com:username/repository_name.git
Это свяжет ваш локальный репозиторий с репозиторием на GitHub.
Создание фронтенда HTMx
Теперь, когда все готово, мы можем приступить к созданию нашего приложения. Мы начнем с фронтенда HTMx, для этого вам нужно создать папку app/templates
в корневой папке проекта.
Затем создайте файл index.html
в папке templates
и заполните его приведенным ниже кодом:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flask HTMX ALPINE App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.5.0"></script>
<style>
body{
padding: 20px;
}
table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 100%;
}
tr.htmx-swapping td {
opacity: 0;
transition: opacity 0.5s ease-out;
}
td, th {
border: 1px solid #383737;
text-align: left;
padding: 8px;
}
tr:nth-child(even) {
background-color: #dddddd;
}
</style>
</head>
<!-- Place <body> </body> code here -->
</html>
В приведенном выше фрагменте кода происходит не так много событий, за исключением строк 5 и 8, которые отвечают за загрузку Bootstrap и HTMx в нашу страницу index.html
. Это дает вам возможность создать интерактивную страницу, просто включив тег <script>
, который ссылается на HTMx, без необходимости устанавливать какие-либо пакеты npm
, как в большинстве SPA. Таким образом, HTMx позволяет создавать более легкие сайты по сравнению со SPA-фреймворками.
Код между тегами <style>
добавляет CSS для стилизации нашего фронтенда, чтобы сделать его более визуально привлекательным. Теперь давайте добавим код, который будет отображаться в теге body нашей страницы. Скопируйте и вставьте приведенный ниже код под тегом </head>
:
<body>
<h1>Book Recommendations</h1>
<form hx-post="/submit" hx-swap="beforeend" hx-target="#new-book" class="mb-3">
<input type="text" placeholder="Book Title" name="title" class="form-control mb-3" />
<input type="text" placeholder="Book Author" name="author" class="form-control mb-3" />
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<table class="table">
<thead>
<tr>
<th scope="col">Book Title</th>
<th scope="col">Book Author</th>
</tr>
</thead>
<tbody id="new-book" hx-target="closest tr" hx-swap="outerHTML swap:0.5s">
{%for book in books%}
<tr>
<td>{{book.Book.title}}</td>
<td>{{book.Author.name}}</td>
<td>
<button class="btn btn-primary"
hx-get="/get-edit-form/{{book.Book.book_id}}">
Edit Title
</button>
</td>
<td>
<button hx-delete="/delete/{{book.Book.book_id}}" class="btn btn-primary">Delete</button>
</td>
</tr>
{%endfor%}
</tbody>
</table>
</body>
Здесь есть несколько атрибутов, которые не используются в традиционном HTML. Давайте рассмотрим их по очереди:
-
hx-[http method] — Примеры этого атрибута включают
hx-post
,hx-get
,hx-put
иhx-delete
. Это способ HTMx обозначить тип запроса, который должен быть отправлен при отправке формы или при возникновении события срабатывания запроса. Эти атрибуты принимают в качестве аргумента маршрут запроса. В случае нашей формы мы используем маршрут/submit
, а кнопки таблицы отправляют запросы по маршрутам/delete
и/get-edit-form
. -
hx-target — Этот атрибут принимает
id
элемента, который вы хотите обновить после успешного запроса или при срабатывании события. Обратите внимание на предшествующее#
, которое написано перед значениемid
.- Вы могли заметить, что мы не использовали значение
id
в таблице, а вместо него использовали значениеclosest tr
. Это меняет ближайшую строку таблицы на HTML, который будет возвращен запросом при выполнении действия. Ближайшая строка всегда будет той же самой строкой, в которой было вызвано событие или запрос, либо кнопкой «Редактировать заголовок», либо кнопкой «Удалить».
- Вы могли заметить, что мы не использовали значение
-
hx-swap — Атрибут hx-swap позволяет указать способ частичной перезагрузки страницы или замены элементов на новые. Он обновляет пользовательский интерфейс в секции, указанной в атрибуте
hx-target
.- В нашей форме мы использовали значение
beforeend
, чтобы сообщить HTMx, что мы хотим добавить результат запроса после последнего дочернего элемента в целевом элементе, которым является таблица сid=new-book
. - Однако в таблице мы использовали значение
outerHTML
, чтобы обозначить, что мы хотим заменить весь элемент<tr>
возвращаемым содержимым. - Полный список допустимых значений
hx-swap
можно посмотреть здесь.
- В нашей форме мы использовали значение
Создание бэкенда Flask
Теперь мы можем приступить к созданию бэкенда нашего приложения. Начните с создания файла run.py
в корневой папке проекта и заполните его приведенным ниже кодом:
from app import app
if __name__ == "__main__":
app.run()
Это точка входа для любого сервера, желающего запустить наше приложение.
Объявление и инициализация модуля app
Создайте файл __init__.py
в папке app
и заполните его приведенным ниже кодом:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///sqlite.db'
app.config["SQLALCHEMY_ECHO"] = False
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
from app import views
from app import models
db.init_app(app)
db.create_all()
Здесь мы объявляем объект нашего приложения, используя пакет Flask, который мы установили ранее. Мы будем использовать базу данных SQLite, которую мы настраиваем с помощью строк app.config
.
Мы используем SQLAlchemy для объявления объекта базы данных, с которым мы будем взаимодействовать, поскольку это позволяет нам читать и писать в базу данных с использованием объектной нотации, что более привычно, чем необработанные операторы SQL. Важно импортировать модели только после объявления объекта базы данных, чтобы соответствующие им таблицы были включены в базу данных при ее создании. Последние две строки выполняют инициализацию базы данных и создание всех соответствующих таблиц в базе данных на основе импортированных моделей.
Регистрация моделей приложений
Следующим шагом будет создание модуля models.py
, который мы импортировали ранее в файле __init__.py
. Создайте файл с именем models.py
в папке app и заполните его приведенным ниже кодом:
from app import db
class Author(db.Model):
author_id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
books = db.relationship("Book", backref="author")
def __repr__(self):
return '<Author: {}>'.format(self.books)
class Book(db.Model):
book_id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey("author.author_id"))
title = db.Column(db.String)
Здесь мы объявляем две модели, которые будем сохранять в нашей базе данных, это Author
и Book
. Стоит отметить отношение «один ко многим» между «автором» и «книгами», поскольку автор может иметь много книг, но каждая книга может иметь только одного автора в контексте данного приложения. Мы обозначаем это отношение с помощью внешнего ключа в поле author_id
книги и backref
в поле books автора.
Создание представлений
Последней частью создания бэкенда нашего приложения является создание представлений или маршрутов, с которыми будет взаимодействовать фронтенд. Создайте файл views.py
в папке app и добавьте в него приведенный ниже код:
from app import app, db
from flask import render_template, request, jsonify
from app.models import Author, Book
@app.route("/", methods=["GET"])
def home():
books = db.session.query(Book, Author).filter(Book.author_id == Author.author_id).all()
return render_template("index.html", books=books)
В приведенном выше фрагменте кода мы добавили маршрут index и привязали к нему функцию home. Функция home сначала запрашивает базу данных, чтобы получить список всех книг, а затем возвращает шаблон index.html
, заполненный списком книг.
Добавьте приведенный ниже код, чтобы добавить маршрут /submit
в файл views.py
нашего приложения:
@app.route("/submit", methods=["POST"])
def submit():
global_book_object = Book()
title = request.form["title"]
author_name = request.form["author"]
author_exists = db.session.query(Author).filter(Author.name == author_name).first()
print(author_exists)
# check if author already exists in db
if author_exists:
author_id = author_exists.author_id
book = Book(author_id=author_id, title=title)
db.session.add(book)
db.session.commit()
global_book_object = book
else:
author = Author(name=author_name)
db.session.add(author)
db.session.commit()
book = Book(author_id=author.author_id, title=title)
db.session.add(book)
db.session.commit()
global_book_object = book
response = f"""
<tr>
<td>{title}</td>
<td>{author_name}</td>
<td>
<button class="btn btn-primary"
hx-get="/get-edit-form/{global_book_object.book_id}">
Edit Title
</button>
</td>
<td>
<button hx-delete="/delete/{global_book_object.book_id}"
class="btn btn-primary">
Delete
</button>
</td>
</tr>
"""
return response
Когда человек отправляет новую книгу, вызывается именно этот маршрут. Сначала логика проверяет, существует ли автор в базе данных, и если да, то сохраняет книгу с author_id
автора. В противном случае создается новый автор, а затем сохраняется книга. Поскольку HTMx ожидает HTML-ответ, метод submit
отвечает строкой HTML-таблицы, которая обновляет список книг на фронтенде. Новая запись будет относиться к недавно добавленной книге.
Далее добавим код для маршрута /delete
. Скопируйте и вставьте приведенный ниже код:
@app.route("/delete/<int:id>", methods=["DELETE"])
def delete_book(id):
book = Book.query.get(id)
db.session.delete(book)
db.session.commit()
return ""
Первое, что вы могли заметить в этом маршруте, это принимаемый параметр запроса id
. Это позволяет нам узнать, какой объект нужно удалить. После удаления книги мы возвращаем пустую строку, что приводит к исчезновению строки, которую мы удалили во фронтенде, так как она заменяется на «ничего».
Теперь у нас есть маршруты для создания, чтения и удаления книг. Пришло время добавить представления, связанные с обновлением записей в книгах, чтобы завершить CRUD-функциональность нашего приложения. Добавьте приведенный ниже код в views.py
, чтобы добавить логику для обновления записей в книгах в ваше приложение:
@app.route("/get-edit-form/<int:id>", methods=["GET"])
def get_edit_form(id):
book = Book.query.get(id)
author = Author.query.get(book.author_id)
response = f"""
<tr hx-trigger='cancel' class='editing' hx-get="/get-book-row/{id}">
<td><input name="title" value="{book.title}"/></td>
<td>{author.name}</td>
<td>
<button class="btn btn-primary" hx-get="/get-book-row/{id}">
Cancel
</button>
<button class="btn btn-primary" hx-put="/update/{id}" hx-include="closest tr">
Save
</button>
</td>
</tr>
"""
return response
@app.route("/get-book-row/<int:id>", methods=["GET"])
def get_book_row(id):
book = Book.query.get(id)
author = Author.query.get(book.author_id)
response = f"""
<tr>
<td>{book.title}</td>
<td>{author.name}</td>
<td>
<button class="btn btn-primary"
hx-get="/get-edit-form/{id}">
Edit Title
</button>
</td>
<td>
<button hx-delete="/delete/{id}"
class="btn btn-primary">
Delete
</button>
</td>
</tr>
"""
return response
@app.route("/update/<int:id>", methods=["PUT"])
def update_book(id):
db.session.query(Book).filter(Book.book_id == id).update({"title": request.form["title"]})
db.session.commit()
title = request.form["title"]
book = Book.query.get(id)
author = Author.query.get(book.author_id)
response = f"""
<tr>
<td>{title}</td>
<td>{author.name}</td>
<td>
<button class="btn btn-primary"
hx-get="/get-edit-form/{id}">
Edit Title
</button>
</td>
<td>
<button hx-delete="/delete/{id}"
class="btn btn-primary">
Delete
</button>
</td>
</tr>
"""
return response
Для логики обновления существует более одного представления, и мы скоро увидим, почему. Маршрут /get-edit-form
вызывается, когда пользователь нажимает на кнопку «Редактировать название» на фронтенде, и возвращает форму для обновления выбранной книги. Если пользователь решает отменить это действие, вызывается маршрут /get-book-row
, который возвращает строку таблицы с неотредактированной записью книги.
Если пользователь продолжает обновление названия книги, то вызывается маршрут /update
. Функция update_book
, связанная с маршрутом /update
, обновляет название книги на основе id
, переданного ей в качестве параметра запроса. После завершения обновления метод возвращает строку HTML-таблицы с обновленным названием книги.
Запуск нашего приложения
Наше приложение готово к тестированию. Перейдите в корневую папку проекта в терминале и выполните следующую команду: python3 run.py
. Это должно запустить сервер разработки на порту 5000. Откройте браузер по адресу http://127.0.0.1:5000/
и вы увидите, что ваше приложение запущено:
Как только вы будете удовлетворены работой приложения, вы можете зафиксировать и отправить изменения на GitHub с помощью следующих команд.
git add .
git commit -m 'commit message'
git push origin
Мы показали вам, как создать приложение Flask HTMx с полным стеком с нуля, и вы должны быть в состоянии развернуть эту базовую версию, но вы можете рассмотреть возможность добавления дополнительных функций для расширения возможностей нашего приложения. Мы рекомендуем вам ознакомиться с Alpine.js, легким JavaScript фреймворком, который хорошо работает с HTMx
для создания более мощных, но при этом легких сайтов.
Полный код, показанный выше, вы можете найти в этом репозитории GitHub.