作为开发人员,CRUD 操作是需要了解的最基本概念之一。今天,我将向您展示如何使用 Django 和 Django Rest 构建 REST API,以及使用 React 构建 SPA,我们将使用它来执行 CRUD 操作。

要求

对于本教程,您需要对 Django 模型、Django Rest序列化程序和ViewSets有基本的了解。

项目设置

首先,要搭建好开发环境。拿起您最喜欢的终端并确保您已安装virtualenv。

完成后,创建一个环境并安装 Django 和 Django rest 框架。

virtualenv --python=/usr/bin/python3.8 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

进入全屏模式 退出全屏模式

所以请确保在settings.py文件中的INSTALLED_APPS中添加menu应用程序和rest_framework

#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

进入全屏模式 退出全屏模式

完成后,让我们创建一个迁移然后应用它。

Migrations是 Django 将您对模型所做的更改(添加字段、删除模型等)传播到数据库架构中的方式。

python manage.py makemigrations
python manage.py migrate

进入全屏模式 退出全屏模式

串行器

Serializers允许我们转换复杂的 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']  

进入全屏模式 退出全屏模式

视图集

Viewsets如果您来自另一个框架,则在 Django 中可以将其称为控制器。

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 已准备就绪。 🙂

[](https://res.cloudinary.com/practicaldev/image/fetch/s--yFDyNVtF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paper-attachments.dropbox。 com/s_91B4E45CE57E19C0F15A00DA56BE11B919EA6BF487475EC7F668C42504558B3E_1610292871973_Screenshot_2021-01-10%2BMenu%2BList%2B%2BDjango%2BREST%2Bframework.png)

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

    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',
    ...
]

进入全屏模式 退出全屏模式

在本教程中,我们将允许所有来源发出跨站点 HTTP 请求。

然而,这是危险的,你不应该在生产中这样做。

# restaurant/settings.py
CORS_ORIGIN_ALLOW_ALL = True

进入全屏模式 退出全屏模式

在生产环境中,您可以改用CORS_ALLOWED_ORIGINS

CORS_ALLOWED_ORIGINS = [
    "https://example.com",
    "https://sub.example.com",
    "http://localhost:3000",
    "http://127.0.0.1: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/文件夹中,确保您有以下文件和目录。

[目录图像](https://res.cloudinary.com/practicaldev/image/fetch/s--MGkD3mEc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/lufaz1kiwh7724enzz7u.png)

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",
    };

进入全屏模式 退出全屏模式

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

    import React from "react";
    import ReactDOM from "react-dom";
    import { BrowserRouter } from "react-router-dom";
    import "./index.css";
    import App from "./App";
    ReactDOM.render(
      <BrowserRouter>
        <App />
      </BrowserRouter>,
      document.getElementById("root")
    );

进入全屏模式 退出全屏模式

完成后,我们可以通过导入bootstrap来修改App.js文件,编写路由,构建主页和导航栏。

    import React from "react";
    import "bootstrap/dist/css/bootstrap.min.css";
    import { Switch, 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 exact to={"/add/"} className="nav-link">
                  Add
                </Link>
              </li>
            </div>
          </nav>
          <div className="container m-10">
            // Add the routes
          </div>
        </div>
      );
    }
    export default App;

进入全屏模式 退出全屏模式

导航栏已经完成,我们已经导入了引导程序和我们需要编写应该映射到我们创建的组件的路由的组件。

    <Switch>
          <Route exact path={["/", "/menus"]} component={MenuList} />
          <Route exact path="/add/" component={AddMenu} />
          <Route path="/menu/:id/update/" component={UpdateMenu} />
    </Switch>

进入全屏模式 退出全屏模式

下一步是为我们的组件编写 CRUD 逻辑和 HTML。

让我们从列出MenuList.js中 API 的菜单开始。

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

  • menus将存储来自 API 的响应对象

  • deleted将包含一个布尔对象以显示一条消息

和三种方法:

  • retrieveAllMenus()从 API 中检索所有菜单并使用setMenus在菜单中设置响应对象。

  • deleteMenu()删除菜单并将deleted状态设置为true,这将有助于我们在每次删除菜单时显示一条简单的消息。

  • handleUpdateClick()导航到新页面以更新菜单。

    import axios from "axios";
    import React, { useState, useEffect, useRef } from "react";
    import { baseURL, headers } from "./../services/menu.service";
    import { useHistory } from "react-router-dom";
    export const MenuList = () => {
      const [menus, setMenus] = useState([]);
      const history = useHistory();
      const countRef = useRef(0);
      const [deleted, setDeleted] = useState(false);
      useEffect(() => {
        retrieveAllMenus();
      }, [countRef]);
      const retrieveAllMenus = () => {
        axios
          .get(`${baseURL}/menu/`, {
            headers: {
              headers,
            },
          })
          .then((response) => {
            setMenus(response.data);
          })
          .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);
          });
      };
      const handleUpdateClick = (id) => {
        history.push(`/menu/${id}/update/`);
      };
    return (
        // ...
      );
    };

进入全屏模式 退出全屏模式

完成后,让我们实现render()方法:

    <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>

进入全屏模式 退出全屏模式

[](https://res.cloudinary.com/practicaldev/image/fetch/s--H2zHBIF4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paper-attachments.dropbox。 com/s_91B4E45CE57E19C0F15A00DA56BE11B919EA6BF487475EC7F668C42504558B3E_1613141850249_Screenshot_2021-02-12%2BReact%2BApp.png)

添加菜单

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 (
        // ...
      );
    };

进入全屏模式 退出全屏模式

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

  • menu默认包含initialMenuState对象的值

  • submitted将包含一个布尔对象以在成功添加菜单时显示一条消息。

和三种方法:

  • handleInputChange()跟踪输入值并设置状态以进行更改。

  • saveMenu()向 API 发送POST请求。

  • 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 onClick={submitMenu} className="btn btn-success">
                Submit
              </button>
            </div>
          )}
        </div>

进入全屏模式 退出全屏模式

更新菜单

该组件将与AddMenu组件有点相同,但是,它将包含一个 get 方法,通过使用对象的id向 API 发出GET请求来检索对象的当前值。

我们使用useHistory()挂钩将id传递给UpdateMenu组件并使用useParams挂钩检索它。

    import axios from "axios";
    import React, { useState, useEffect, useRef } 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,
      };
      let { id } = useParams();
      const [currentMenu, setCurrentMenu] = useState(initialMenuState);
      const [submitted, setSubmitted] = useState(false);
      const countRef = useRef(0);
      useEffect(() => {
        retrieveMenu();
      }, [countRef]);
      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按钮,您将被重定向到具有此组件的新页面,并且字段中的默认值。

[查看演示](https://res.cloudinary.com/practicaldev/image/fetch/s--M0YjbdDy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://www.dropbox .com/s/wu4pdt0hyf7kae6/road.mp4%3Fdl%3D0)

结论

在本文中,我们学习了使用 Django 和 React 构建一个 CRUD 应用程序 web。由于每篇文章都可以做得更好,因此欢迎您在评论部分提出建议或问题。 😉

在这个repo中检查所有这篇文章的代码。

本文已原发于我的博客

Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐