Наталья Кайда 19 сентября 2022

🐍🚀 Пишем гибридное приложение для хранения заметок на Django, Django Ninja REST Framework и Alpine.js

Изучаем основные возможности Django Ninja, Alpine.js и Axios в процессе создания веб-приложения для хранения заметок.
🐍🚀 Пишем гибридное приложение для хранения заметок на Django, Django Ninja REST Framework и Alpine.js

Рано или поздно любой начинающий Django-разработчик сталкивается с проектом, для которого нужно четкое разделение приложения на бэкенд и фронтенд: в этом случае серверную часть пишут на Django REST Framework (DRF) или FastAPI, а клиентскую – на React, Angular или Vue. Если речь идет о высоконагруженном сайте со множеством интерактивных элементов на стороне клиента – такой подход неизбежен. При этом значительную часть функциональности, которую Django предоставляет по умолчанию, придется реализовать на стороне фронтенда – и это будет гораздо сложнее.

Но если нагрузка на приложение будет умеренной, а идея разделения на фронтенд и бэкенд возникла из-за необходимости реализовать взаимодействие с базой данных без постоянных обновлений страниц – то такой проект целесообразнее воплотить в виде гибридного приложения. Гибридный подход позволяет:

  • максимально использовать «батарейки» Django – в том числе формы, систему аутентификации и авторизации;
  • реализовать асинхронную передачу данных и CRUD без перезагрузки страниц;
  • включить в шаблоны Джанго любые интерактивные JS-элементы.

Обзор проекта

Главная страница Notes – категории заметок
Главная страница Notes – категории заметок

Мы создадим гибридное приложение Notes, которое опирается на базовую функциональность Django. Помимо Джанго, приложение будет использовать:

  • Фреймворк Django Ninja для API и CRUD.
  • Библиотеку Axios – для HTTP-запросов к бэкенду.
  • Ультралегкий JS-фреймворк Alpine.js и CSS-фреймворк Bootstrap – для фронтенда.
Заметки в отдельной категории
Заметки в отдельной категории

Приложение использует один базовый и два гибридных шаблона Django/Alpine.js:

  • base.html – подключает библиотеку Axios, а также фреймворки Alpine.js и Bootstrap.
  • index.html – выводит все категории заметок. Карточки категорий добавляются и удаляются без перезагрузки страницы.
  • detail.html – отображает все заметки в определенной категории. Карточки заметок добавляются и удаляются без перезагрузки, таким же образом происходит обновление статуса В процессе на Сделано.

Весь код для проекта находится в репозитории.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»

Бэкенд и API

Для разработки API мы воспользуемся новым фреймворком Django Ninja. Это отличная альтернатива Django REST Framework и FastAPI, причем по синтаксису Django Ninja ближе к последнему. Django Ninja гораздо проще DRF и намного производительнее.

<a href="https://github.com/vitalik/django-ninja-benchmarks" target="_blank" rel="noopener noreferrer nofollow">Тест производительности Django Ninja</a>
Тест производительности Django Ninja

Единственный недостаток Django Ninja – пока что фреймворк не поддерживает представления на основе классов, и код получается довольно объемным (по сравнению с DRF), но в ближайшее время разработчики обещают решить эту задачу.

Начнем работу с создания нового проекта и приложения:

        python -m venv Notes\venv
cd notes
venv\scripts\activate
pip install django
pip install django-ninja
django-admin startproject config .
manage.py startapp notes

    

Создадим базу данных и учетную запись админа:

        manage.py migrate
manage.py createsuperuser

    

Сделаем нужные настройки в файле config/settings.py:

        import os
…
INSTALLED_APPS = [
…
	'notes',
]
…
STATIC_URL = '/static/'
STATIC_ROOT='/'
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

    

Создадим файл notes/urls.py с содержимым:

notes/urls.py
        from django.urls import path
from . import views
app_name = 'notes'
urlpatterns = [
	path('', views.home, name='home'),
    path('category/<category_id>/', views.category_detail, name='detail'),
]

    

Добавим нужные маршруты в config/urls.py:

config/urls.py
        from django.contrib import admin
from django.urls import path, include
from notes.api import api
from django.conf import settings
from django.conf.urls.static import static
 
urlpatterns = [
	path('admin/', admin.site.urls),
	path("api/", api.urls),
	path('', include('notes.urls', namespace="notes")),
]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

    

Создадим модели для заметок и категорий:

notes/models.py
        from django.db import models

class Category(models.Model):
    title = models.CharField(max_length=100)
    description = models.CharField(max_length=300)
    created = models.DateTimeField(auto_now_add=True)
   
    class Meta:
    	verbose_name_plural = 'Категории'
    	ordering = ['created']

    def __str__(self):
        return self.title

class Note(models.Model):
    title = models.CharField(max_length=250)
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='notes')
    created = models.DateTimeField(auto_now_add=True)
    completed = models.BooleanField(default=False, blank=True)
    
    class Meta:
    	verbose_name_plural = 'Заметки'
    	ordering = ['-created']  

    def __str__(self):
        return self.title        
    

Зарегистрируем модели в admin.py:

admin.py
        from django.contrib import admin
from .models import Category, Note
 
admin.site.register(Category)
admin.site.register(Note)

    

Файл notes/views.py должен выглядеть так:

notes/views.py
        from django.shortcuts import render, get_object_or_404

from .models import Category, Note

def home(request):
    return render(request, 'index.html', {
        'categories': Category.objects.all()
    })


def category_detail(request, category_id):
    category = get_object_or_404(Category, id=category_id)
    return render(request, 'detail.html', {
        'category': category
    })

    

Теперь нужно подготовить и выполнить миграции:

        manage.py makemigrations
manage.py migrate

    

После выполнения миграций можно приступать к разработке API и CRUD-операций. Для этого нужно создать два файла – notes/schemas.py и notes/api.py. Сначала займемся схемами – они в Django Ninja выполняют те же самые функции, что и сериализатор в Django REST Framework, то есть определяют, какие именно данные поступают в базу и какие запрашиваются. Обратите внимание на разницу в наборах данных между схемами NoteIn, NoteOut, NoteUpd, CategoryIn, CategoryOut:

notes/schemas.py
        from ninja import Schema, ModelSchema
from datetime import date
from .models import Note


class CategoryIn(Schema):
    title: str
    description: str

class CategoryOut(Schema):
    id: int
    title: str
    description: str    
    created: date
  

class NoteIn(ModelSchema):
    class Config:
        model = Note
        model_fields = ['title', 'category']

class NoteUpd(ModelSchema):
    class Config:
        model = Note
        model_fields = ['id', 'completed']

class NoteOut(ModelSchema):
    class Config:
        model = Note
        model_fields = ['id','title', 'category', 'created', 'completed']

    

Вся функциональность API описана в одном файле – notes/api.py:

notes/api.py
        from datetime import date
from typing import List
from ninja import NinjaAPI, Schema
from django.shortcuts import get_object_or_404
from .models import Note, Category
from .schemas import NoteIn, NoteOut, NoteUpd, CategoryIn, CategoryOut


api = NinjaAPI()


@api.post("/notes", tags=['Заметки'])
def create_note(request, payload: NoteIn):
    data = payload.dict()
    category = Category.objects.get(id=data['category'])
    del data['category']
    note = Note.objects.create(category=category, **data)
    return {"id": note.id}

@api.post("/category", tags=['Категории'])
def create_category(request, payload: CategoryIn):
    category = Category.objects.create(**payload.dict())
    return {"id":category.id}    


@api.get("/notes/{note_id}", response=NoteOut, tags=['Заметки'])
def get_note(request, note_id: int):
    note = get_object_or_404(Note, id=note_id)
    return note

@api.get("/category/{category_id}", response=CategoryOut, tags=['Категории'])
def get_category(request, category_id: int):
    category = get_object_or_404(Category, id=category_id)
    return category

@api.get("/category", response=List[CategoryOut], tags=['Категории'])
def list_categories(request):
    categories = Category.objects.all()
    return categories   

@api.get("/notes", response=List[NoteOut], tags=['Заметки'])
def list_notes(request):
    notes = Note.objects.all()
    return notes


@api.patch("/notes/{note_id}", tags=['Заметки'])
def update_note(request, note_id: int, payload: NoteUpd):
    note = get_object_or_404(Note, id=note_id)
    for attr, value in payload.dict().items():
        setattr(note, attr, value)
    note.save()
    return {"success": True}

@api.put("/category/{category_id}", tags=['Категории'])
def update_category(request, category_id: int, payload: CategoryIn):
    note = get_object_or_404(Category, id=category_id)
    for attr, value in payload.dict().items():
        setattr(note, attr, value)
    category.save()
    return {"success": True}

@api.delete("/notes/{note_id}", tags=['Заметки'])
def delete_note(request, note_id: int):
    note = get_object_or_404(Note, id=note_id)
    note.delete()
    return {"success": True}

@api.delete("/category/{category_id}", tags=['Категории'])
def delete_category(request, category_id: int):
    category = get_object_or_404(Category, id=category_id)
    category.delete()
    return {"success": True}

    

Теперь можно запустить сервер и протестировать работу API:

        manage.py runserver
    

Перейдите по ссылке http://localhost:8000/api/docs – это адрес Django Ninja API:

Веб-интерфейс Django Ninja API
Веб-интерфейс Django Ninja API

Создавать новые категории и заметки можно прямо на этой странице. Выберите операцию POST в разделе Категории, нажмите кнопку Try it out, введите название и описание категории:

Добавление в базу новой категории
Добавление в базу новой категории

Кликните на Execute – готово, первая категория добавлена в базу данных:

Сервер сообщает об успешном добавлении категории
Сервер сообщает об успешном добавлении категории

HTTP-запросы к бэкенду

За обработку запросов к бэкенду отвечает библиотека Axios, подключенная в шаблоне base.html. Axios – это альтернатива fetch с более дружественным синтаксисом. Код HTTP-запросов расположен в конце шаблонов index.html и detail.html. Создайте папку notes/templates и поместите туда все три шаблона. Кроме того, создайте папку static на одном уровне с notes и config, и сохраните в ней файл CSS-стилей.

Перейдите на главную страницу приложения http://localhost:8000/ и протестируйте работу API и Axios – теперь карточки категорий и заметки можно добавлять с фронтенда:

Добавление карточек происходит без перезагрузки страницы
Добавление карточек происходит без перезагрузки страницы
Статус заметки изменяется одним кликом без перезагрузки
Статус заметки изменяется одним кликом без перезагрузки

Фронтенд

Помимо API и Axios в добавлении элементов без перезагрузки страницы участвует фреймворк Alpine.js. Синтаксис Alpine.js очень похож на Vue.js – но, в отличие от Vue, Alpine не конфликтует с тегами Django и не требует заключения кода в теги {% verbatim %} {% endverbatim %}. По функциональности Alpine максимально близок к jQuery, поэтому фреймворк уже окрестили «современным jQuery».

Синтаксис Alpine.js во многом напоминает синтаксис стандартного шаблонизатора Django, и разобраться в нем (в отличие от ванильного JavaScript) не составит никакого труда. Еще одно огромное преимущество Alpine.js заключается в том, что его не нужно никуда устанавливать и запускать на отдельном локальном сервере (и, следовательно, не придется использовать CORS).

Более того, Alpine.js без проблем может получить данные от шаблонизатора Django. Обратите внимание на эти фрагменты в index.html, где в шаблоне мирно уживаются запрос Alpine и получение данных из бэкенда Django:

        <div x-data="getCategories()">
   <h3 class="text-center mt-5" style="color:#777">все категории заметок пользователя <span class="fw-bold">{{ request.user.username }}</span></h3>
    <form id="category-form">
    	{% csrf_token %}
	</form>
...
const getCategories = () => {
	return {
    	newCategory: '',
    	newDescription: '',
    	categories: [
        	{% for category in categories %}
        	{ 'title': '{{ category.title }}', 'id': '{{ category.id }}', 'description': '{{ category.description }}' },
        	{% endfor %}
        ]
    }
};

    

В конце шаблонов index.html и detail.html библиотека Axios обеспечивает обработку запросов к Django Ninja API. При создании новой категории Axios принимает от Alpine название и описание (title, description) и передает API запрос POST:

        const addCategory = async (title, description) => {
	try {
	const res = await axios.post('/api/category',
    	{ title, description },
    	{ headers: { 'X-CSRFToken': csrftoken }}
    	);
	location.reload();
  	} catch (e) {
    	console.error(e);
  	}
};

    

Для удаления категории Axios передает бэкенду соответствующий ID – categoryId:

        const removeCategory = async categoryId => {
	try {
	const res = await axios.delete('/api/category/' + categoryId,
    	{ headers: { 'X-CSRFToken': csrftoken }}
    	);
	location.reload();
  	} catch (e) {
    	console.error(e);
  	}
};

    

Подведем итоги

Фреймворк Django Ninja и библиотека Alpine.js появились совсем недавно, но уже успели произвести фурор среди разработчиков: скорость, гибкость, простота синтаксиса и бесшовная интеграция делают их идеальным выбором для гибридных Django-проектов с умеренной нагрузкой. Приложение Notes, несмотря на простоту, позволяет быстро изучить все основные возможности Django Ninja и Alpine.js, разобраться в механизме взаимодействия API и Axios, и приступить к разработке более сложных проектов. Напоминаем, что весь код для Notes можно взять в репозитории.

***

Материалы по теме

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Аналитик данных
Екатеринбург, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ