Для разработчика операции 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">×</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">×</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">×</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. И так как каждую статью можно сделать лучше, ваши предложения или вопросы приветствуются в разделе комментариев. 😉
Проверьте код всех этих статей в этом репозитории.
Эта статья была первоначально опубликована в моем блоге