🐍🚀 Пишем гибридное приложение для хранения заметок на Django, Django Ninja REST Framework и Alpine.js
Изучаем основные возможности Django Ninja, Alpine.js и Axios в процессе создания веб-приложения для хранения заметок.
Рано или поздно любой начинающий Django-разработчик сталкивается с проектом, для которого нужно четкое разделение приложения на бэкенд и фронтенд: в этом случае серверную часть пишут на Django REST Framework (DRF) или FastAPI, а клиентскую – на React, Angular или Vue. Если речь идет о высоконагруженном сайте со множеством интерактивных элементов на стороне клиента – такой подход неизбежен. При этом значительную часть функциональности, которую Django предоставляет по умолчанию, придется реализовать на стороне фронтенда – и это будет гораздо сложнее.
Но если нагрузка на приложение будет умеренной, а идея разделения на фронтенд и бэкенд возникла из-за необходимости реализовать взаимодействие с базой данных без постоянных обновлений страниц – то такой проект целесообразнее воплотить в виде гибридного приложения. Гибридный подход позволяет:
- максимально использовать «батарейки» Django – в том числе формы, систему аутентификации и авторизации;
- реализовать асинхронную передачу данных и CRUD без перезагрузки страниц;
- включить в шаблоны Джанго любые интерактивные JS-элементы.
https://t.me/python_job_interview – подготовиться к python собеседованию
Обзор проекта
Мы создадим гибридное приложение 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 и намного производительнее.
Единственный недостаток 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:
Создавать новые категории и заметки можно прямо на этой странице. Выберите операцию 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 можно взять в репозитории.
https://t.me/ai_machinelearning_big_data