Предисловие

Веб-разработчики часто сталкиваются с необходимостью динамически обновлять страницы без полной перезагрузки. С этим хорошо справляется технология ассинхронного обмена данными AJAX, однако я не нашел на просторах интернета простого мануала использования AJAX и решил создать его сам. В этой статье я собираюсь подробно показать взаимодействие фронтенда с AJAX и бекенда с Django, ограничившись минимумом кода. Статья больше рассчитана на новичков и станет отличной базой для дальнейшего развития в теме.

Часть 1. Подготовка проекта

Для демонстрации я создам небольшой Django проект, который имеет всего лишь одно приложение, обрабатывающее сообщение от пользователя. Вы можете скачать проект с GitHub отсюда или создать что-то подобное самостоятельно. Познакомимся с структурой приложения поближе.

Структура приложения

app/ 
│ 
│ ├── app/ 
│  └── #Конфигурация проекта

│ ├── modules/ #Здесь хранятся приложения 
│  └── messages_app/ 
│   ├── models.py #Хранит модель Message (Имя отправителя, Текст, Время отправки) 
│   ├── forms.py #Хранит форму модели Message  (Имя отправителя, Текст сообщения)
│   ├── urls.py #Два адреса, обрабатывают views
│   ├── views.py #Может отправить на шаблон отображения сообщений (messages.html) или на шаблон отправки     сообщения (send_message.html) 
│   └── # Другие настройки по умолчанию

│ ├── templates/ #Здесь хранится весь фронт 
│  ├── messages.html # Обрабатывает все сообщения 
│  ├── send_message.html # Отправка сообщения для сохранение его в модель Message
│  └── src/ 
│   └── js/ 
│    └── ajax.js/ #Здесь будет хранится код JS, пока файл пуст

Все django-приложения будут храниться в modules, а html-шаблоны в templates в папке проекта, код JS будет хранится в templates/src/js.

Дальше я подробно разберу файлы внутри структуры проекта: приложения и шаблоны. Если вы в состоянии разобраться самостоятельно, то можете смело пропустить этот шаг и перейти ко второй части.

Рассмотрим единственную модель приложения:

#/modules/models.py

from django.db import models

class Message(models.Model):
    sender_name = models.CharField(max_length=100)  # Имя отправителя
    message = models.TextField()  # Текст сообщения
    sent_at = models.DateTimeField(auto_now_add=True)  # Время отправки

Я уже создал два тестовых сообщения для отображения. Эта модель используется во views.py, для отображения всех элементов и для создания нового элемента:

#/modules/views.py

from django.shortcuts import render
from .models import Message
from .forms import MessageForm


# Create your views here.
def get_all_mesages(request):
    messages = Message.objects.all()
    return render(request, 'messages_app/messages.html', {'messages': messages} )

def send_message(request):
    if request.method == 'POST':
        form = MessageForm(request.POST)
        if form.is_valid():
            form.save()  # Сохраняем форму в базе данных
            return render(request, 'messages_app/send_message.html', {'form': form, 'repeat': True})
    else:
        form = MessageForm()

    return render(request, 'messages_app/send_message.html', {'form': form})

Во views используется форма из forms.py:

#/modules/forms.py

from django import forms
from .models import Message

class MessageForm(forms.ModelForm):
    class Meta:
        model = Message
        fields = ['sender_name', 'message']

HTML-шаблоны

Используемые шаблоны - message.html для отображения всех элементов:

<!-- /templates/message.html -->
{% load static %}

<div id="messages-container">
    {% for message in messages %}
        <hr>
        <p>Отправитель: {{ message.sender_name }}</p>
        <p>Сообщение: {{ message.message }}</p>
        <p>Отправлено: {{ message.sent_at }}</p>
        <hr>
    {% endfor %}
</div>

<script src="{% static 'messages_app/src/js/ajax.js' %}"></script>

И send_message.html:

<!-- /templates/send_message.html -->
{% if repeat %}
    <div> Вы уже отправляли форму, хотите отправить повторно?</div>
{% endif %}

<h1>Отправить сообщение</h1>
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Отправить</button>
</form>

По пути templates/src/js/ находится ajax.js, который сейчас пуст. В дальнейшем мы заполним его кодом.

Часть 2. Внедрение AJAX

Внутри AJAX-запроса используется стандартный HTTP-запрос, такой же, как и обычный GET или POST, однако нам, для эффективного использования AJAX, данные нужно завернуть в JSON-формат. Для этого мы будем использовать JsonResponse на бекенде - перейдем во views.py и создадим еще один метод, который будет заворачивать экземпляры модели в JSON.

#/modules/views.py

#...Ранее добавленные импорты и методы

from django.http import JsonResponse

def ajax_messages(request):
    messages = Message.objects.all() #Получим все сообщения из модели

    #Обработаем модель
    messages_data = []
    for message in messages:
        messages_data.append({
            'sender_name': message.sender_name,
            'message': message.message,
            'sent_at': message.sent_at.strftime('%Y-%m-%d %H:%M:%S'),  # Преобразуем дату в строку
        })
    return JsonResponse({'messages': messages_data})

Добавим отображение метода в urls.py

#/modules/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.get_all_mesages, name='get_all_messages'),
    path('send/', views.send_message, name='send_message'),

    #ajax
    path('get_data/', views.ajax_messages, name='get_data')
]

Попробуем перейти по ссылке https:localhost/get_data/. Нас ждет такой результат:

localhost/get_data/
localhost/get_data/

Теперь мы можем получать по этому адресу данные из модели в JSON-форматe. Далее создадим AJAX-запрос - для этого на странице отображения напишем следующий JS код и поместим его в ajax.js

// templates/src/js/ajax.js 

let get_data_url = 'get_data/' // Переход на ссылку с ajax запросом

function draw_messages(messages) { // То что мы будем делать с нашими данными
  console.log(messages) // Сейчас мы просто хотим вывести их в консоль
}


function ajax_get(){
  return fetch(get_data_url, {
      method: 'GET',
  }).then(response => response.json()) // Преобразуем полученный ответ в JSON
    .then(data => draw_messages(data)) // Обрабатываем данные, полученные в ответе с помощью функции draw_message [3 строка]
    .catch(error => console.error('Ошибка:', error)); // Если возникли ошибки, выводим их в консоль
}

Мы создали асинхронный GET-запрос который обращается к адресу /get_data/. Уже сейчас, если в консоли браузера на странице отображения сообщений мы введем ajax_get() - получим тот самый ответ в json - элементы модели Message.

Получаем object { messages: ...}
Получаем object { messages: ...}

Нам нужно, чтобы метод обновлял все содержимое блока #messages-container, для этого изменим метод draw_messages внутри ajax.js

// templates/src/js/ajax.js 
let get_data_url = 'get_data/' // Переход на ссылку с ajax запросом

const messagesContainer = document.querySelector('#messages-container'); // Сюда помещаются наши статьи

function draw_messages(messages) {
  // Очистим контейнер перед добавлением новых сообщений
  messagesContainer.innerHTML = '';

  // Для каждого сообщения в массиве messages создаем HTML
  messages.forEach(message => {
      const messageElement = document.createElement('div');
      
      // Создаем элементы для отображения данных сообщения
      const senderName = document.createElement('p');
      senderName.textContent = `Отправитель: ${message.sender_name}`;
      
      const messageText = document.createElement('p');
      messageText.textContent = `Сообщение: ${message.message}`;
      
      const sentAt = document.createElement('p');
      sentAt.textContent = `Отправлено: ${message.sent_at}`;
      
      // Создаем разделитель
      const hr = document.createElement('hr');
      
      // Добавляем все элементы в messageElement
      messageElement.appendChild(senderName);
      messageElement.appendChild(messageText);
      messageElement.appendChild(sentAt);
      messageElement.appendChild(hr);
      
      // Вставляем messageElement в контейнер
      messagesContainer.appendChild(messageElement);
  });
}

function ajax_get() {
  fetch(get_data_url, {
      method: 'GET', // Используем метод 'GET'
  })
  .then(response => response.json()) // Преобразуем ответ в JSON
  .then(data => {
      draw_messages(data.messages); // Передаем полученные сообщения в draw_messages
  })
  .catch(error => console.error('Ошибка:', error)); // Обрабатываем ошибки
}

При AJAX-запросе мы полностью очищаем #messages-container и обновляем содержимым из GET-запроса.

Эта простая и местами примитивная реализация поможет понять всю суть процесса, однако сейчас все равно чего-то не хватает - добавим интервал в 1 секунду для ajax_get(), чтобы данные обновлялись автоматически, и нам не пришлось перезагружать страницу. Для этого добавим в конец ajax.js:

// templates/src/js/ajax.js 

// ...Переменные и методы

setInterval(ajax_get, 1000);

Проверить результат можно просто: откройте страницу с отображением сообщений и страницу с отправкой. Заполните форму отправки и вернитесь на страницу с сообщениями, она автоматически обновится.

В этой статье мы рассмотрели простой GET-запрос. Однако такой подход не подойдет, если объем данных в модели будет слишком большим. В таком случае нужно реализовать POST-запрос с сохранением текущих данных на клиенте. Об этом могу написать позже, если статья вам понравиться, и вы захотите продолжения!

Комментарии (5)


  1. Sergey_Kh
    01.12.2024 13:38

    Такое можно делать с использованием htmx буквально одной строчкой в шаблоне, не нужно столько js кода.


    1. Neychychyen Автор
      01.12.2024 13:38

      Спасибо, не знал про htmx, но похоже зря, интересная библиотека


  1. Ipatov_e
    01.12.2024 13:38

    Видимо, опечатка в коде:

    let get_data_url = 'get_data/'

    Этот кусок кода как будто не отсюда. Лучше константой объявить. А так же в стиль написания должен быть camelCase, как у всех остальных дальше по коду.

    const getDataUrl = 'get_data/';


    1. Neychychyen Автор
      01.12.2024 13:38

      Согласен, лучше всего и правильнее передать ее вместе с контекстом вajax_messages (ИЗ /modules/views.py), но я решил не обременять статью излишними объяснениями и дополнениями в коде и просто скопировал ссылку из бекенда, чтобы сосредоточить читателя на самом методе.


  1. ilya_chch
    01.12.2024 13:38

    1. js в templates - не самое хорошее решение. для этого есть static.

    2. Django-приложения в modules. зачем modules? при условии, что в корне проекта будет app, modules, templates и manage.py, создание дополнительного неймспейса, кажется, не дает никакого выйгрыша.

    3. app в названии приложений.

    4. Про HTMX уже говорили. Я согласен, что он был бы тут очень кстати. Но, как вариант, можно рассмотреть Alpine Ajax

      • еще как вариант использовать django-render-block, чтобы было удобнее работать с htmx/alpine