Создание полностекового приложения с помощью Flask и HTMx

[В этом руководстве объясняется, как создать приложение с помощью 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.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *