Изучаем основные возможности Django Ninja, Alpine.js и Axios в процессе создания веб-приложения для хранения заметок. Рано или поздно любой начинающий Django-разработчик сталкивается с проектом, для которого нужно четкое разделение приложения на бэкенд и фронтенд: в этом случае серверную часть пишут на Django REST Framework (DRF) или FastAPI, а клиентскую – на React, Angular или Vue. Если речь идет о высоконагруженном сайте со множеством интерактивных элементов на стороне клиента – такой подход неизбежен. При этом значительную часть функциональности, которую Django предоставляет по умолчанию, придется реализовать на стороне фронтенда – и это будет гораздо сложнее. Но если нагрузка на приложение будет умеренной, а идея разделения на фронтенд и бэкенд возникла из-за необходимости реализовать взаимодействие с базой данных без постоянных обновлений страниц – то такой проект целесообразнее воплотить в виде гибридного приложения. Гибридный подход позволяет: максимально использовать «батарейки» Django – в том числе формы, систему аутентификации и авторизации; реализовать асинхронную передачу данных и CRUD без перезагрузки страниц; включить в шаблоны Джанго любые интерактивные JS-элементы. Обзор проекта Главная страница 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 и намного производительнее. Тест производительности Django Ninja Единственный недостаток Django Ninja – пока что фреймворк не поддерживает представления на основе классов, и код получается довольно объемным (по сравнению с DRF), но в ближайшее время разработчики обещают решить эту задачу. Начнем работу с создания нового проекта и приложения: python -m venv Notesvenv cd notes venvscriptsactivate 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 Создавать новые категории и заметки можно прямо на этой странице. Выберите операцию 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 можно взять в репозитории. *** Материалы по теме 🐍🚀 Django с нуля. Часть 1: пишем многопользовательский блог для клуба любителей задач Python 🐍🚀 Создаем рекрутинговый портал на Django: часть 1 🔩 Полный фуллстек: пишем сайт на Django, Vue и GraphQL
Рано или поздно любой начинающий Django-разработчик сталкивается с проектом, для которого нужно четкое разделение приложения на бэкенд и фронтенд: в этом случае серверную часть пишут на Django REST Framework (DRF) или FastAPI, а клиентскую – на React, Angular или Vue. Если речь идет о высоконагруженном сайте со множеством интерактивных элементов на стороне клиента – такой подход неизбежен. При этом значительную часть функциональности, которую Django предоставляет по умолчанию, придется реализовать на стороне фронтенда – и это будет гораздо сложнее.
Но если нагрузка на приложение будет умеренной, а идея разделения на фронтенд и бэкенд возникла из-за необходимости реализовать взаимодействие с базой данных без постоянных обновлений страниц – то такой проект целесообразнее воплотить в виде гибридного приложения. Гибридный подход позволяет:
Главная страница Notes – категории заметок
Мы создадим гибридное приложение Notes, которое опирается на базовую функциональность Django. Помимо Джанго, приложение будет использовать:
Заметки в отдельной категории
Приложение использует один базовый и два гибридных шаблона Django/Alpine.js:
Весь код для проекта находится в репозитории.
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста» Интересно, перейти к каналу
Для разработки API мы воспользуемся новым фреймворком Django Ninja. Это отличная альтернатива Django REST Framework и FastAPI, причем по синтаксису Django Ninja ближе к последнему. Django Ninja гораздо проще DRF и намного производительнее.
Тест производительности Django Ninja
Единственный недостаток Django Ninja – пока что фреймворк не поддерживает представления на основе классов, и код получается довольно объемным (по сравнению с DRF), но в ближайшее время разработчики обещают решить эту задачу.
Начнем работу с создания нового проекта и приложения:
python -m venv Notesvenv cd notes venvscriptsactivate 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
Создавать новые категории и заметки можно прямо на этой странице. Выберите операцию POST в разделе Категории, нажмите кнопку Try it out, введите название и описание категории:
Добавление в базу новой категории
Кликните на Execute – готово, первая категория добавлена в базу данных:
Сервер сообщает об успешном добавлении категории
За обработку запросов к бэкенду отвечает библиотека 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».
{% verbatim %} {% endverbatim %}
Синтаксис 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:
(title, description)
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:
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 можно взять в репозитории.
***
Ваш адрес email не будет опубликован. Обязательные поля помечены *
Сохранить моё имя, email и адрес сайта в этом браузере для последующих моих комментариев.
Δ
Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.