Создание CRUD-приложения с использованием Django, React и Docker — 2022

Для разработчика операции CRUD являются одной из самых фундаментальных концепций. Сегодня мы узнаем, как построить REST API с помощью Django и Django Rest и SPA с помощью React, которые мы будем использовать для выполнения CRUD-операций.

Настройка проекта

Во-первых, мы должны настроить среду разработки. Возьмите свой любимый терминал и убедитесь, что у вас установлен virtualenv.
Как только это будет сделано, создайте среду и установите Django и Django rest framework.

virtualenv --python=/usr/bin/python3.10 venv
source venv/bin/activate
pip install django django-rest-framework
Войдите в полноэкранный режим Выход из полноэкранного режима

После установки пакетов мы можем создать проект и начать работу.

django-admin startproject restaurant .
Войти в полноэкранный режим Выход из полноэкранного режима

Примечание: Не забудьте о точке в конце этой команды. Она создаст каталоги и файлы в текущем каталоге вместо того, чтобы разрабатывать их в новом каталоге, restaurant.
Чтобы убедиться, что проект был хорошо инициирован, попробуйте python manage.py runserver. И выберите 127.0.0.1:8000.

Теперь давайте создадим приложение Django.

python manage.py startapp menu
Вход в полноэкранный режим Выход из полноэкранного режима

Итак, убедитесь, что добавили приложение menu и rest_framework в INSTALLED_APPS в файле settings.py.

#restaurant/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework',
    'menu'
    ]
Вход в полноэкранный режим Выйдите из полноэкранного режима

Хорошо. Мы можем начать работать над логикой, которую мы хотим достичь в этом уроке. Итак, мы напишем Menu :

  • Модель
  • Сериализатор
  • ViewSet
  • И, наконец, настроим маршруты.

Модель

Модель Menu будет содержать только 5 полей.

#menu/models.py
from django.db import models

class Menu(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField()
    price = models.IntegerField()
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.name
Вход в полноэкранный режим Выход из полноэкранного режима

Когда все готово, давайте создадим миграцию и применим ее.

Миграции — это способ Django распространить изменения, внесенные в модели (добавление поля, удаление поля, удаление таблицы, создание таблицы и т.д.) в вашу базу данных.

python manage.py makemigrations
python manage.py migrate
Вход в полноэкранный режим Выход из полноэкранного режима

Сериализаторы

Сериализаторы позволяют нам преобразовывать сложные структуры данных Django, такие как querysets или экземпляры моделей в нативные объекты Python, которые могут быть преобразованы в формат JSON/XML.
Мы создадим сериализатор для преобразования наших данных в формат JSON.

#menu/serializers.py

from rest_framework import serializers
from menu.models import Menu

class MenuSerializer(serializers.ModelSerializer):
    class Meta:
        model = Menu
        fields = ['id', 'name', 'description', 'price', 'created', 'updated']  
Вход в полноэкранный режим Выход из полноэкранного режима

Наборы представлений

Наборы представлений могут называться контроллерами, если вы пришли из другого фреймворка.
ViewSet — это концепция, разработанная DRF, которая заключается в группировке набора представлений для данной модели в одном классе Python.

Этот набор представлений соответствует предопределенным действиям типа CRUD (Create, Read, Update, Delete), связанным с методами HTTP.

Каждое из этих действий является методом экземпляра ViewSet. Среди этих предопределенных действий мы находим

  • список
  • получить
  • обновить
  • уничтожить
  • частичное_обновление
  • создать
#menu/viewsets.py
from rest_framework import viewsets
from menu.models import Menu
from menu.serializers import MenuSerializer

class MenuViewSet(viewsets.ModelViewSet):
    serializer_class = MenuSerializer

    def get_queryset(self):
        return Menu.objects.all()
Вход в полноэкранный режим Выход из полноэкранного режима

Отлично. У нас есть логика, но мы должны добавить конечные точки API.

Сначала создайте файл routers.py.


#./routers.py
from rest_framework import routers
from menu.viewsets import MenuViewSet
router = routers.SimpleRouter()
router.register(r'menu', MenuViewSet, basename='menu')


#restaurant/urls.py
from django.contrib import admin
from django.urls import path, include

from routers import router

urlpatterns = [
    # path('admin/', admin.site.urls),
    path('api/', include((router.urls, 'restaurant'), namespace='restaurant'))
]
Вход в полноэкранный режим Выход из полноэкранного режима

Если вы еще не запустили свой сервер.

python manage.py runserver
Войти в полноэкранный режим Выйдите из полноэкранного режима

Затем нажмите http://127.0.0.1:8000/api/menu/ в вашем браузере.
Ваш просматриваемый API готов 🙂 .

Давайте добавим CORS-ответы. Добавление CORS-заголовков позволяет другим доменам получить доступ к ресурсам API.

    pip install django-cors-headers
Вход в полноэкранный режим Выход из полноэкранного режима

Затем добавьте его в INSTALLED_APPS.

# restaurant/settings.py
INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]
Войти в полноэкранный режим Выйти из полноэкранного режима

Вам также потребуется добавить промежуточный класс для прослушивания ответов.

#restaurant/settings.py
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]
Вход в полноэкранный режим Выход из полноэкранного режима

Мы разрешим запросы, приходящие с localhost:3000 и 127.0.0.1:3000, поскольку внешний React-сервер будет работать по этим адресам.

# restaurant/settings.py

# CORS HEADERS
CORS_ALLOWED_ORIGINS = [
    'http://127.0.0.1:3000',
    'http://localhost:3000'
]
Вход в полноэкранный режим Выход из полноэкранного режима

Потребление React.js CRUD REST API

Убедитесь, что у вас установлена последняя версия create-react-app.

yarn create-react-app restaurant-menu-front
cd restaurant-menu-front
yarn start
Войти в полноэкранный режим Выйдите из полноэкранного режима

Затем откройте http://localhost:3000/, чтобы проверить работающее приложение.
Теперь мы можем добавить зависимости этого проекта.

yarn add axios bootstrap react-router-dom
Войти в полноэкранный режим Выйти из полноэкранного режима

С помощью этой строки команды мы установили :

  • axios: HTTP-клиент на основе обещаний
  • bootstrap: библиотека для создания прототипа приложения без написания большого количества CSS
  • react-router-dom : библиотека React для маршрутов в нашем приложении.

Внутри папки src/ убедитесь, что у вас есть следующие файлы и каталоги.

В директории src/components/ у нас есть три компонента :

  • AddMenu.js
  • UpdateMenu.js
  • MenuList.js

А в директории src/services/ создайте menu.service.js и следующие строки :

    export const baseURL = "http://localhost:8000/api";
    export const headers = {
      "Content-type": "application/json",
    };
Вход в полноэкранный режим Выйти из полноэкранного режима

Обязательно импортируйте react-router-dom в ваш файл index.js и оберните App в объект BrowserRouter.

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
Войдите в полноэкранный режим Выход из полноэкранного режима

После этого мы можем изменить файл App.js, импортировав bootstrap, написать маршруты и построить главную страницу и панель навигации.

import React from "react";
import "bootstrap/dist/css/bootstrap.min.css";
import { Routes, Route, Link } from "react-router-dom";

import { AddMenu } from "./components/AddMenu";
import { MenuList } from "./components/MenuList";
import { UpdateMenu } from "./components/UpdateMenu";

function App() {
  return (
    <div>
      <nav className="navbar navbar-expand navbar-dark bg-info">
        <a href="/" className="navbar-brand">
          Restaurant Menu
        </a>
        <div className="navbar-nav mr-auto">
          <li className="nav-item">
            <Link to={"/add/"} className="nav-link">
              Add a menu
            </Link>
          </li>
        </div>
      </nav>

      <div className="container m-10">
// Adding the routes       
      </div>
    </div>
  );
}

export default App;
Вход в полноэкранный режим Выход из полноэкранного режима

Нам нужно написать маршруты, которые должны связываться с созданным нами компонентом.

<div className="container m-10">
  <Routes>
    <Route path="/" element={<MenuList />} />
    <Route path="/add/" element={<AddMenu />} />
    <Route path="/menu/:id/update/" element={<UpdateMenu />} />
  </Routes>
</div>
Войти в полноэкранный режим Выход из полноэкранного режима

Следующий шаг — написание CRUD-логики и HTML для наших компонентов.
Начнем с перечисления меню из API в MenuList.js.

Для этого скрипта у нас будет два состояния :

, и три метода :

import axios from "axios";
import React, { useState, useEffect } from "react";
import { baseURL, headers } from "./../services/menu.service";
import { useNavigate } from "react-router-dom";

export const MenuList = () => {
  const [menus, setMenus] = useState([]);
  const navigate = useNavigate();

  const [deleted, setDeleted] = useState(false);

  const retrieveAllMenus = () => {
    axios
      .get(`${baseURL}/menu/`, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setMenus(response.data);
        console.log(menus);
      })
      .catch((e) => {
        console.error(e);
      });
  };

  const deleteMenu = (id) => {
    axios
      .delete(`${baseURL}/menu/${id}/`, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setDeleted(true);
        retrieveAllMenus();
      })
      .catch((e) => {
        console.error(e);
      });
  };


  useEffect(() => {
    retrieveAllMenus();
  }, [retrieveAllMenus]);

  const handleUpdateClick = (id) => {
    navigate(`/menu/${id}/update/`);
  };
    return (
        // ...
      );
    };
Войти в полноэкранный режим Выйти из полноэкранного режима

Когда все будет готово, давайте установим метод return():

    <div className="row justify-content-center">
          <div className="col">
            {deleted && (
              <div
                className="alert alert-danger alert-dismissible fade show"
                role="alert"
              >
                Menu deleted!
                <button
                  type="button"
                  className="close"
                  data-dismiss="alert"
                  aria-label="Close"
                >
                  <span aria-hidden="true">&times;</span>
                </button>
              </div>
            )}
            {menus &&
              menus.map((menu, index) => (
                <div className="card my-3 w-25 mx-auto">
                  <div className="card-body">
                    <h2 className="card-title font-weight-bold">{menu.name}</h2>
                    <h4 className="card-subtitle mb-2">{menu.price}</h4>
                    <p className="card-text">{menu.description}</p>
                  </div>
                  <div classNameName="card-footer">
                    <div
                      className="btn-group justify-content-around w-75 mb-1 "
                      data-toggle="buttons"
                    >
                      <span>
                        <button
                          className="btn btn-info"
                          onClick={() => handleUpdateClick(menu.id)}
                        >
                          Update
                        </button>
                      </span>
                      <span>
                        <button
                          className="btn btn-danger"
                          onClick={() => deleteMenu(menu.id)}
                        >
                          Delete
                        </button>
                      </span>
                    </div>
                  </div>
                </div>
              ))}
          </div>
        </div>
Вход в полноэкранный режим Выход из полноэкранного режима

Добавить меню

Компонент AddMenu.js содержит форму для отправки нового меню. Она содержит три поля: name, description & price.

import axios from "axios";
import React, { useState } from "react";
import { baseURL, headers } from "./../services/menu.service";

export const AddMenu = () => {
  const initialMenuState = {
    id: null,
    name: "",
    description: "",
    price: 0,
  };

  const [menu, setMenu] = useState(initialMenuState);
  const [submitted, setSubmitted] = useState(false);

  const handleMenuChange = (e) => {
    const { name, value } = e.target;
    setMenu({ ...menu, [name]: value });
  };

  const submitMenu = () => {
    let data = {
      name: menu.name,
      description: menu.description,
      price: menu.price,
    };

    axios
      .post(`${baseURL}/menu/`, data, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setMenu({
          id: response.data.id,
          name: response.data.name,
          description: response.data.description,
          price: response.data.price,
        });
        setSubmitted(true);
        console.log(response.data);
      })
      .catch((e) => {
        console.error(e);
      });
  };

  const newMenu = () => {
    setMenu(initialMenuState);
    setSubmitted(false);
  };
    return (
        // ...
      );
    };
Вход в полноэкранный режим Выйти из полноэкранного режима

Для этого скрипта у нас будет два состояния :

И три метода :

<div className="submit-form">
  {submitted ? (
    <div>
      <div
        className="alert alert-success alert-dismissible fade show"
        role="alert"
      >
        Menu Added!
        <button
          type="button"
          className="close"
          data-dismiss="alert"
          aria-label="Close"
        >
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <button className="btn btn-success" onClick={newMenu}>
        Add
      </button>
    </div>
  ) : (
    <div>
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          type="text"
          className="form-control"
          id="name"
          required
          value={menu.name}
          onChange={handleMenuChange}
          name="name"
        />
      </div>

      <div className="form-group">
        <label htmlFor="description">Description</label>
        <input
          type="text"
          className="form-control"
          id="description"
          required
          value={menu.description}
          onChange={handleMenuChange}
          name="description"
        />
      </div>

      <div className="form-group">
        <label htmlFor="price">Price</label>
        <input
          type="number"
          className="form-control"
          id="price"
          required
          value={menu.price}
          onChange={handleMenuChange}
          name="price"
        />
      </div>

      <button
        type="submit"
        onClick={submitMenu}
        className="btn btn-success mt-2"
      >
        Submit
      </button>
    </div>
  )}
</div>
Войти в полноэкранный режим Выход из полноэкранного режима

Обновление меню

Компонент будет немного идентичен компоненту AddMenu. Но он будет содержать метод get для получения текущего значения объекта путем выполнения запроса GET к API с id объекта.
Мы используем хук useHistory(), чтобы передать id компоненту UpdateMenu и получить его с помощью хука useParams.

import axios from "axios";
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { baseURL, headers } from "./../services/menu.service";

export const UpdateMenu = () => {
  const initialMenuState = {
    id: null,
    name: "",
    description: "",
    price: 0,
  };

  const { id } = useParams();

  const [currentMenu, setCurrentMenu] = useState(initialMenuState);
  const [submitted, setSubmitted] = useState(false);

  useEffect(() => {
    retrieveMenu();
  }, []);

  const handleMenuChange = (e) => {
    const { name, value } = e.target;
    setCurrentMenu({ ...currentMenu, [name]: value });
  };

  const retrieveMenu = () => {
    axios
      .get(`${baseURL}/menu/${id}/`, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setCurrentMenu({
          id: response.data.id,
          name: response.data.name,
          description: response.data.description,
          price: response.data.price,
        });
        console.log(currentMenu);
      })
      .catch((e) => {
        console.error(e);
      });
  };

  const updateMenu = () => {
    let data = {
      name: currentMenu.name,
      description: currentMenu.description,
      price: currentMenu.price,
    };

    axios
      .put(`${baseURL}/menu/${id}/`, data, {
        headers: {
          headers,
        },
      })
      .then((response) => {
        setCurrentMenu({
          id: response.data.id,
          name: response.data.name,
          description: response.data.description,
          price: response.data.price,
        });
        setSubmitted(true);
        console.log(response.data);
      })
      .catch((e) => {
        console.error(e);
      });
  };

  const newMenu = () => {
    setCurrentMenu(initialMenuState);
    setSubmitted(false);
  };
  return (
          // ...
      );
    };
Вход в полноэкранный режим Выход из полноэкранного режима

А это код внутри return :

<div className="submit-form">
  {submitted ? (
    <div>
      <div
        className="alert alert-success alert-dismissible fade show"
        role="alert"
      >
        Menu Updated!
        <button
          type="button"
          className="close"
          data-dismiss="alert"
          aria-label="Close"
        >
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <button className="btn btn-success" onClick={newMenu}>
        Update
      </button>
    </div>
  ) : (
    <div>
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          type="text"
          className="form-control"
          id="name"
          required
          value={currentMenu.name}
          onChange={handleMenuChange}
          name="name"
        />
      </div>

      <div className="form-group">
        <label htmlFor="description">Description</label>
        <input
          type="text"
          className="form-control"
          id="description"
          required
          value={currentMenu.description}
          onChange={handleMenuChange}
          name="description"
          default
        />
      </div>

      <div className="form-group">
        <label htmlFor="price">Price</label>
        <input
          type="number"
          className="form-control"
          id="price"
          required
          value={currentMenu.price}
          onChange={handleMenuChange}
          name="price"
        />
      </div>

      <button onClick={updateMenu} className="btn btn-success">
        Submit
      </button>
    </div>
  )}
</div>
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь все готово.

Если вы нажмете на кнопку Update на карточке меню, вы будете перенаправлены на новую страницу с этим компонентом, со значениями по умолчанию в полях.

Сборка Docker (необязательно)

Docker + Docker Compose (необязательно)

Docker — это открытая платформа для разработки, доставки и запуска приложений внутри контейнеров.

Зачем использовать Docker?
Он помогает отделить приложения от инфраструктуры и быстрее доставлять код.

Если вы впервые работаете с Docker, я настоятельно рекомендую вам пройти краткое обучение и прочитать документацию по нему.

Вот несколько отличных ресурсов, которые помогли мне:

  • Учебник по Docker
  • Учебная программа по Docker

Конфигурация Docker для API

Dockerfile представляет собой текстовый документ, содержащий все команды, которые можно вызвать в командной строке для создания образа.

Добавьте Dockerfile в корень проекта Django:

# pull official base image
FROM python:3.10-alpine

# set work directory
WORKDIR /app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install psycopg2 dependencies
RUN apk update 
 && apk add gcc python3-dev

# install python dependencies
COPY requirements.txt /app/requirements.txt
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt


# copy project
COPY . .
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь мы начали с Docker-образа для Python на основе Alpine. Это легкий дистрибутив Linux, разработанный для обеспечения безопасности и экономии ресурсов.
После этого мы задали рабочий каталог, а затем две переменные окружения:

1 — PYTHONDONTWRITEBYTECODE для предотвращения записи Python файлов .pyc на диск.
2 — PYTHONUNBUFFERED для предотвращения буферизации stdout и stderr.

После этого мы выполняем такие операции, как:

  • Настройка переменных окружения
  • Установка пакета сервера PostgreSQL
  • Копирование файла requirements.txt в путь приложения, обновление pip и установка пакета python для запуска нашего приложения
  • И последнее копирование всего проекта

Также добавим файл .dockerignore.

env
venv
Dockerfile
Вход в полноэкранный режим Выход из полноэкранного режима

Docker Compose для API

Docker Compose — отличный инструмент (<3). Вы можете использовать его для определения и запуска многоконтейнерных Docker-приложений.

Что нам нужно? Ну, просто YAML-файл, содержащий всю конфигурацию сервисов нашего приложения.
Затем, с помощью команды docker-compose, мы можем создать и запустить все эти сервисы.

Этот файл будет использоваться для разработки.

version: '3.9'
services:
 api:
 container_name: menu_api
 build: .
 restart: always
 env_file: .env
 ports:
      - "8000:8000"
 command: >
      sh -c " python manage.py migrate &&
          gunicorn restaurant.wsgi:application --bind 0.0.0.0:8000"
 volumes:
     - .:/app
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте добавим gunicorn и некоторые конфигурации перед сборкой нашего образа.

pip install gunicorn
Войти в полноэкранный режим Выход из полноэкранного режима

И добавим его в качестве требования в requirements.txt.
Вот как выглядит мой файл requirements.txt :

django==4.0.4
django-cors-headers==3.12.0
djangorestframework==3.13.1
gunicorn==20.1.0
Войти в полноэкранный режим Выход из полноэкранного режима

Настройка завершена. Давайте соберем наши контейнеры и проверим, все ли работает локально.

docker-compose up -d --build
Вход в полноэкранный режим Выход из полноэкранного режима

Ваш проект будет запущен на https://localhost:8000/.

Dockerfile для приложения React

Добавьте Dockerfile в корень проекта React:

FROM node:17-alpine

WORKDIR /app

COPY package.json ./

COPY yarn.lock ./

RUN yarn install --frozen-lockfile

COPY . .
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь мы начали с Docker-образа для JavaScript, основанного на Alpine. Это легкий дистрибутив Linux, разработанный для обеспечения безопасности и экономии ресурсов.

Также добавим файл .dockerignore.

node_modules
npm-debug.log
Dockerfile
yarn-error.log
Вход в полноэкранный режим Выход из полноэкранного режима

И добавим код для docker-compose.yaml.

version: "3.9"

services:
 react-app:
 container_name: react_app
 restart: on-failure
 build: .
 volumes:
      - ./src:/app/src
 ports:
      - "3000:3000"
 command: >
      sh -c "yarn start"
Вход в полноэкранный режим Выход из полноэкранного режима

Настройка завершена. Давайте соберем наши контейнеры и проверим, все ли работает локально.

docker-compose up -d --build 
Вход в полноэкранный режим Выход из полноэкранного режима

Ваш проект будет запущен на https://localhost:3000/. И вуаля! Мы докеризировали API и React-приложения.🚀.

Заключение

В этой статье мы научились создавать CRUD-приложения с помощью Django и React. И так как каждую статью можно сделать лучше, ваши предложения или вопросы приветствуются в разделе комментариев. 😉
Проверьте код всех этих статей в этом репозитории.

Эта статья была первоначально опубликована в моем блоге

Оцените статью
Procodings.ru
Добавить комментарий