使用 Django、React 和 Docker 构建 CRUD 应用程序 - 2022
作为开发者,CRUD 操作是最基本的概念之一。今天,我们将学习如何使用 Django 和 Django Rest 构建 REST API,以及如何使用 React 构建单页应用 (SPA),并用它来执行 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:
- 模型
- 序列化器
- 视图集
- 最后,配置路由。
模型
该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称为 Controller。ViewSet
是 DRF 开发的一个概念,它将给定模型的一组视图组合到一个 Python 类中。
这组视图对应于与 HTTP 方法关联的 CRUD 类型(创建、读取、更新、删除)的预定义操作。
这些操作都是 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:一个基于 Promise 的 HTTP 客户端
- Bootstrap:一个无需编写大量 CSS 代码即可创建应用原型的库
- react-router-dom:一个用于应用程序路由的 React 库。
文件夹内src/请确保包含以下文件和目录。
该src/components/目录中包含三个组件:
AddMenu.jsUpdateMenu.jsMenuList.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。
对于这个脚本,我们将有两个状态:
menus它将存储来自 API 的响应对象。deleted其中将包含一个布尔对象,用于显示消息。
以及三种方法:
retrieveAllMenus()使用 API 获取所有菜单,并将响应对象设置到菜单中setMenus。deleteMenu()删除菜单并将deleted状态设置为true,这将有助于我们在每次删除菜单时显示一条简单的消息。handleUpdateClick()跳转到新页面以更新菜单。
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 (
// ...
);
};
对于这个脚本,我们将有两个状态:
menuinitialMenuState默认情况下,它将包含对象的值。submitted将包含一个布尔对象,用于在添加菜单时显示消息。
以及三种方法:
handleInputChange()跟踪输入值并设置状态以进行更改。saveMenu()POST向 API发送请求。newMenu()显示成功消息后,允许用户再次添加新菜单。
<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,我强烈建议你快速浏览一下教程并阅读一些相关文档。
以下是一些对我帮助很大的资源:
API 的 Docker 配置
这Dockerfile表示一个文本文件,其中包含所有可以在命令行中调用以创建图像的命令。
在 Django 项目根目录下添加 Dockerfile:
# 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 . .
首先,我们使用了一个基于 Alpine 的 Python Docker 镜像。Alpine 是一个轻量级的 Linux 发行版,注重安全性和资源效率。
之后,我们设置了一个工作目录,并设置了两个环境变量:
1 -PYTHONDONTWRITEBYTECODE防止 Python 将.pyc文件写入磁盘
2 -PYTHONUNBUFFERED防止 Python 缓冲stdout和stderr
之后,我们执行如下操作:
- 设置环境变量
- 安装 PostgreSQL 服务器软件包
- 将文件复制
requirements.txt到应用程序路径,升级 pip,并安装 Python 包以运行我们的应用程序。 - 最后复制整个项目
另外,我们再添加一个.dockerignore文件。
env
venv
Dockerfile
用于 API 的 Docker Compose
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/。
React 应用的 Dockerfile
在 React 项目根目录下添加 Dockerfile:
FROM node:17-alpine
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
这里,我们首先使用了一个基于 Alpine 的 JavaScript Docker 镜像。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 应用容器化了。🚀
结论
在本文中,我们学习了如何使用 Django 和 React 构建 CRUD Web 应用。每篇文章都有改进的空间,欢迎在评论区提出您的建议或问题。😉 您可以在此代码库
中查看所有文章的代码。
这篇文章最初发表在我的博客上。
文章来源:https://dev.to/koladev/build-a-crud-application-using-django-react-docker-2022-11f4


