发布于 2026-01-06 1 阅读
0

使用 Django、React 和 Docker 构建 CRUD 应用程序 - 2022

使用 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
Enter fullscreen mode Exit fullscreen mode

安装完软件包后,我们就可以创建项目并开始工作了。

django-admin startproject restaurant .
Enter fullscreen mode Exit fullscreen mode

注意:不要忘记命令末尾的点号。它会在当前目录中生成目录和文件,而不是在新目录中创建它们restaurant
为了确保项目已正确启动,请尝试运行命令python manage.py runserver并按回车键127.0.0.1:8000

现在让我们创建一个 Django 应用。

python manage.py startapp menu
Enter fullscreen mode Exit fullscreen mode

menu所以请务必将应用程序添加rest_frameworkINSTALLED_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'
    ]
Enter fullscreen mode Exit fullscreen mode

很好。我们可以开始着手实现本教程中想要达成的逻辑了。那么,我们将编写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
Enter fullscreen mode Exit fullscreen mode

完成后,我们来创建迁移并应用它。

迁移是 Django 将对模型所做的更改(添加字段、删除字段、删除表、创建表等)传播到数据库的一种方式。

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

序列化器

序列化器允许我们将复杂的 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']  
Enter fullscreen mode Exit fullscreen mode

视图集

如果您之前使用过其他框架,可以将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()
Enter fullscreen mode Exit fullscreen mode

太好了。逻辑部分我们已经搭建好了,但还需要添加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'))
]
Enter fullscreen mode Exit fullscreen mode

如果你还没有启动服务器。

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

然后http://127.0.0.1:8000/api/menu/在浏览器中点击。
您的可浏览 API 已准备就绪。🙂

让我们添加 CORS 响应。添加 CORS 标头允许其他域访问 API 资源。

    pip install django-cors-headers
Enter fullscreen mode Exit fullscreen mode

然后,将其添加到INSTALLED_APPS

# restaurant/settings.py
INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]
Enter fullscreen mode Exit fullscreen mode

您还需要添加一个中间件类来监听响应。

#restaurant/settings.py
MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]
Enter fullscreen mode Exit fullscreen mode

我们将允许来自以下地址的请求localhost:3000127.0.0.1:3000因为前端 React 服务器将在这些地址运行。

# restaurant/settings.py

# CORS HEADERS
CORS_ALLOWED_ORIGINS = [
    'http://127.0.0.1:3000',
    'http://localhost:3000'
]
Enter fullscreen mode Exit fullscreen mode

React.js CRUD REST API 的使用

请确保您已安装最新版本的 create-react-app。

yarn create-react-app restaurant-menu-front
cd restaurant-menu-front
yarn start
Enter fullscreen mode Exit fullscreen mode

然后打开http://localhost:3000/查看正在运行的应用程序。
现在我们可以添加该项目的依赖项了。

yarn add axios bootstrap react-router-dom
Enter fullscreen mode Exit fullscreen mode

通过这行命令,我们安装了:

  • axios:一个基于 Promise 的 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",
    };
Enter fullscreen mode Exit fullscreen mode

请确保在文件react-router-dom中导入index.js并将其包装AppBrowserRouter对象中。

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>
);
Enter fullscreen mode Exit fullscreen mode

完成后,我们可以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;
Enter fullscreen mode Exit fullscreen mode

我们需要编写路由,这些路由应该映射到我们创建的组件。

<div className="container m-10">
  <Routes>
    <Route path="/" element={<MenuList />} />
    <Route path="/add/" element={<AddMenu />} />
    <Route path="/menu/:id/update/" element={<UpdateMenu />} />
  </Routes>
</div>
Enter fullscreen mode Exit fullscreen mode

下一步是编写组件的 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 (
        // ...
      );
    };
Enter fullscreen mode Exit fullscreen mode

完成后,我们来实施该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>
Enter fullscreen mode Exit fullscreen mode

添加菜单

AddMenu.js组件包含一个用于提交新菜单的表单。它包含三个字段:namedescriptionprice

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 (
        // ...
      );
    };
Enter fullscreen mode Exit fullscreen mode

对于这个脚本,我们将有两个状态:

  • 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">&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>
Enter fullscreen mode Exit fullscreen mode

更新菜单

该组件与现有组件略有不同AddMenu。但是,它将包含一个 GET 方法,用于通过GET向 API 发送请求并传入对象的id参数来获取对象的当前值。
我们使用useHistory()钩子函数将参数传递idUpdateMenu组件,并使用钩子函数来获取它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 (
          // ...
      );
    };
Enter fullscreen mode Exit fullscreen mode

这是代码内部的内容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>
Enter fullscreen mode Exit fullscreen mode

现在一切就绪。

如果您点击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 . .
Enter fullscreen mode Exit fullscreen mode

首先,我们使用了一个基于 Alpine 的 Python Docker 镜像。Alpine 是一个轻量级的 Linux 发行版,注重安全性和资源效率。
之后,我们设置了一个工作目录,并设置了两个环境变量:

1 -PYTHONDONTWRITEBYTECODE防止 Python 将.pyc文件写入磁盘
2 -PYTHONUNBUFFERED防止 Python 缓冲stdoutstderr

之后,我们执行如下操作:

  • 设置环境变量
  • 安装 PostgreSQL 服务器软件包
  • 将文件复制requirements.txt到应用程序路径,升级 pip,并安装 Python 包以运行我们的应用程序。
  • 最后复制整个项目

另外,我们再添加一个.dockerignore文件。

env
venv
Dockerfile
Enter fullscreen mode Exit fullscreen mode

用于 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
Enter fullscreen mode Exit fullscreen mode

gunicorn在构建镜像之前,让我们添加一些配置。

pip install gunicorn
Enter fullscreen mode Exit fullscreen mode

同时将其添加为一项要求requirements.txt
以下是我的requirements.txt文件内容:

django==4.0.4
django-cors-headers==3.12.0
djangorestframework==3.13.1
gunicorn==20.1.0
Enter fullscreen mode Exit fullscreen mode

配置已完成。接下来我们构建容器,并在本地测试一切是否正常。

docker-compose up -d --build
Enter fullscreen mode Exit fullscreen mode

您的项目将在 上运行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 . .
Enter fullscreen mode Exit fullscreen mode

这里,我们首先使用了一个基于 Alpine 的 JavaScript Docker 镜像。Alpine 是一个轻量级的 Linux 发行版,旨在提高安全性和资源利用效率。

另外,我们再添加一个.dockerignore文件。

node_modules
npm-debug.log
Dockerfile
yarn-error.log
Enter fullscreen mode Exit fullscreen mode

接下来我们添加代码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"
Enter fullscreen mode Exit fullscreen mode

配置已完成。接下来我们构建容器,并在本地测试一切是否正常。

docker-compose up -d --build 
Enter fullscreen mode Exit fullscreen mode

您的项目将在 . 上运行https://localhost:3000/。瞧!我们已经将 API 和 React 应用容器化了。🚀

结论

在本文中,我们学习了如何使用 Django 和 React 构建 CRUD Web 应用。每篇文章都有改进的空间,欢迎在评论区提出您的建议或问题。😉 您可以在此代码库
中查看所有文章的代码

这篇文章最初发表在我的博客上。

文章来源:https://dev.to/koladev/build-a-crud-application-using-django-react-docker-2022-11f4