如何使用Django和React构建待办事项应用程序
介绍
在本教程中,您将使用 Django 和 React 构建一个待办事项应用程序。
React 是一个用于开发 SPA(单页应用程序)的 JavaScript 框架。 它有 可靠的文档 和围绕它的充满活力的生态系统。
Django 是一个 Python Web 框架,它简化了 Web 开发中的常见做法。 Django 是可靠的,并且还拥有一个充满活力的稳定库生态系统,支持常见的开发需求。
对于此应用程序,React 充当前端或客户端框架,处理用户界面并通过对 Django 后端的请求获取和设置数据,后者是使用 Django REST 框架 (DRF) 构建的 API。
在本教程结束时,您将拥有一个完整的应用程序:
注意:本教程的源代码在GitHub上可用。
警告: 本教程中提供的代码用于教育目的,不用于生产用途。
此应用程序将允许用户创建任务并将其标记为完成或未完成。
先决条件
要学习本教程,您需要:
本教程已使用 Python v3.9.1、pip
v20.2.4、Django v3.1.6、djangorestframework
v3.12.2、django-cors-headers
v3.7.0、Node v15.8.0、 npm
v7.5.4、React v17.0.1 和 axios
v0.21.0。
第 1 步 — 设置后端
在本节中,您将创建一个新的项目目录并安装 Django。
打开一个新的终端窗口并运行以下命令来创建一个新的项目目录:
mkdir django-todo-react
接下来,导航到目录:
cd django-todo-react
现在使用 pip
安装 Pipenv:
pip install pipenv
注意: 根据您的安装,您可能需要使用 pip3
而不是 pip
。
并激活一个新的虚拟环境:
pipenv shell
使用 Pipenv 安装 Django:
pipenv install django
然后创建一个名为 backend
的新项目:
django-admin startproject backend
接下来,导航到新创建的后端目录:
cd backend
启动一个名为 todo
的新应用程序:
python manage.py startapp todo
运行迁移:
python manage.py migrate
并启动服务器:
python manage.py runserver
在 Web 浏览器中导航到 http://localhost:8000
:
此时,您将看到一个 Django 应用程序的实例成功运行。 完成后,您可以停止服务器(CONTROL+C
或 CTRL+C
)。
注册 todo
应用程序
现在您已经完成了后端的设置,您可以开始将 todo
应用程序注册为已安装的应用程序,以便 Django 可以识别它。
在代码编辑器中打开 backend/settings.py
文件并将 todo
添加到 INSTALLED_APPS
:
后端/settings.py
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'todo', ]
然后,保存您的更改。
定义 Todo
模型
让我们创建一个模型来定义 Todo
项目应如何存储在数据库中。
在代码编辑器中打开 todo/models.py
文件并添加以下代码行:
待办事项/models.py
from django.db import models # Create your models here. class Todo(models.Model): title = models.CharField(max_length=120) description = models.TextField() completed = models.BooleanField(default=False) def _str_(self): return self.title
上面的代码片段描述了 Todo 模型的三个属性:
title
description
completed
completed
属性是任务的状态。 任务将在任何时候完成或未完成。 因为你已经创建了一个 Todo
模型,你需要创建一个迁移文件:
python manage.py makemigrations todo
并将更改应用到数据库:
python manage.py migrate todo
您可以使用 Django 默认提供的管理界面进行测试,以查看 CRUD 操作是否适用于您创建的 Todo
模型。
使用代码编辑器打开 todo/admin.py
文件并添加以下代码行:
待办事项/admin.py
from django.contrib import admin from .models import Todo class TodoAdmin(admin.ModelAdmin): list_display = ('title', 'description', 'completed') # Register your models here. admin.site.register(Todo, TodoAdmin)
然后,保存您的更改。
您需要创建一个“超级用户”帐户才能访问管理界面。 在终端中运行以下命令:
python manage.py createsuperuser
系统将提示您输入超级用户的用户名、电子邮件和密码。 请务必输入您能记住的详细信息,因为您将需要它们来登录管理仪表板。
再次启动服务器:
python manage.py runserver
在 Web 浏览器中导航到 http://localhost:8000/admin
。 并使用之前创建的用户名和密码登录:
您可以使用此界面创建、编辑和删除 Todo
项目:
试用此接口后,可以停止服务器(CONTROL+C
或CTRL+C
)。
第 2 步 — 设置 API
在本节中,您将使用 Django REST 框架创建一个 API。
使用 Pipenv 安装 djangorestframework
和 django-cors-headers
:
pipenv install djangorestframework django-cors-headers
您需要将 rest_framework
和 corsheaders
添加到已安装的应用程序列表中。 在代码编辑器中打开 backend/settings.py
文件并更新 INSTALLED_APPS
和 MIDDLEWARE
部分:
后端/settings.py
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'corsheaders', 'rest_framework', 'todo', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware', ]
然后,将这些代码行添加到 backend/settings.py
文件的底部:
后端/settings.py
CORS_ORIGIN_WHITELIST = [ 'http://localhost:3000' ]
django-cors-headers
是一个 Python 库,可防止您通常会因 CORS 规则而出现的错误。 在 CORS_ORIGIN_WHITELIST
代码中,您将 localhost:3000
列入白名单,因为您希望应用程序的前端(将在该端口上提供服务)与 API 交互。
创建 serializers
您将需要序列化程序将模型实例转换为 JSON,以便前端可以处理接收到的数据。
使用您的代码编辑器创建一个 todo/serializers.py
文件。 打开 serializers.py
文件并使用以下代码行更新它:
待办事项/序列化程序.py
from rest_framework import serializers from .models import Todo class TodoSerializer(serializers.ModelSerializer): class Meta: model = Todo fields = ('id', 'title', 'description', 'completed')
此代码指定要使用的模型以及要转换为 JSON 的字段。
创建视图
您需要在 todo/views.py
文件中创建一个 TodoView
类。
使用代码编辑器打开 todo/views.py
文件并添加以下代码行:
待办事项/views.py
from django.shortcuts import render from rest_framework import viewsets from .serializers import TodoSerializer from .models import Todo # Create your views here. class TodoView(viewsets.ModelViewSet): serializer_class = TodoSerializer queryset = Todo.objects.all()
viewsets
基类默认提供 CRUD 操作的实现。 此代码指定 serializer_class
和 queryset
。
使用您的代码编辑器打开 backend/urls.py
文件,并将内容替换为以下代码行:
后端/urls.py
from django.contrib import admin from django.urls import path, include from rest_framework import routers from todo import views router = routers.DefaultRouter() router.register(r'todos', views.TodoView, 'todo') urlpatterns = [ path('admin/', admin.site.urls), path('api/', include(router.urls)), ]
此代码指定 API 的 URL 路径。 这是完成 API 构建的最后一步。
您现在可以在 Todo
模型上执行 CRUD 操作。 路由器类允许您进行以下查询:
/todos/
- 返回所有Todo
项目的列表。 可以在此处执行CREATE
和READ
操作。/todos/id
- 使用id
主键返回单个Todo
项目。 可以在此处执行UPDATE
和DELETE
操作。
让我们重新启动服务器:
python manage.py runserver
在 Web 浏览器中导航到 http://localhost:8000/api/todos
:
您可以使用界面 CREATE
一个新的待办事项:
如果 Todo 项创建成功,您将看到成功的响应:
您还可以使用 id
主键对特定 Todo
项目执行 DELETE
和 UPDATE
操作。 使用地址结构 /api/todos/{id}
并提供 id
。
将 1
添加到 URL 以检查 id
为“1”的待办事项。 在 Web 浏览器中导航到 http://localhost:8000/api/todos/1
:
这样就完成了应用后端的搭建。
第 3 步 — 设置前端
现在您已经完成了应用程序的后端,您可以创建前端并让它通过您创建的接口与后端通信。
首先,打开一个新的终端窗口并导航到 django-todo-react
项目目录。
要设置前端,本教程将依赖 Create React App。 有几种方法可以使用 create-react-app
。 一种方法是使用 npx
运行包并创建项目:
npx create-react-app frontend
您可以通过阅读 如何使用 Create React App 设置 React 项目来了解有关此方法的更多信息。
工程创建完成后,可以切换到新建的frontend
目录:
cd frontend
然后,启动应用程序:
npm start
您的 Web 浏览器将打开 http://localhost:3000
,您将看到默认的 Create React App 屏幕:
接下来,安装 bootstrap
和 reactstrap
以提供用户界面工具。
npm install bootstrap@4.6.0 reactstrap@8.9.0 --legacy-peer-deps
注意: 根据您的 React、Bootstrap 和 Reactstrap 版本,您可能会遇到 unable to resolve dependency tree
错误。
在修订时,最新版本的 popper.js
已被弃用,将与 React 17+ 冲突。 这是 已知问题 ,安装时可以使用 --legacy-peer-deps
选项。
在代码编辑器中打开 index.js
并添加 bootstrap.min.css
:
前端/src/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import 'bootstrap/dist/css/bootstrap.css'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
如果这一步有困难,可以查阅官方文档添加bootstrap。
在代码编辑器中打开 App.js
并添加以下代码行:
前端/src/App.js
import React, { Component } from "react"; const todoItems = [ { id: 1, title: "Go to Market", description: "Buy ingredients to prepare dinner", completed: true, }, { id: 2, title: "Study", description: "Read Algebra and History textbook for the upcoming test", completed: false, }, { id: 3, title: "Sammy's books", description: "Go to library to return Sammy's books", completed: true, }, { id: 4, title: "Article", description: "Write article on how to use Django with React", completed: false, }, ]; class App extends Component { constructor(props) { super(props); this.state = { viewCompleted: false, todoList: todoItems, }; } displayCompleted = (status) => { if (status) { return this.setState({ viewCompleted: true }); } return this.setState({ viewCompleted: false }); }; renderTabList = () => { return ( <div className="nav nav-tabs"> <span className={this.state.viewCompleted ? "nav-link active" : "nav-link"} onClick={() => this.displayCompleted(true)} > Complete </span> <span className={this.state.viewCompleted ? "nav-link" : "nav-link active"} onClick={() => this.displayCompleted(false)} > Incomplete </span> </div> ); }; renderItems = () => { const { viewCompleted } = this.state; const newItems = this.state.todoList.filter( (item) => item.completed == viewCompleted ); return newItems.map((item) => ( <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center" > <span className={`todo-title mr-2 ${ this.state.viewCompleted ? "completed-todo" : "" }`} title={item.description} > {item.title} </span> <span> <button className="btn btn-secondary mr-2" > Edit </button> <button className="btn btn-danger" > Delete </button> </span> </li> )); }; render() { return ( <main className="container"> <h1 className="text-white text-uppercase text-center my-4">Todo app</h1> <div className="row"> <div className="col-md-6 col-sm-10 mx-auto p-0"> <div className="card p-3"> <div className="mb-4"> <button className="btn btn-primary" > Add task </button> </div> {this.renderTabList()} <ul className="list-group list-group-flush border-top-0"> {this.renderItems()} </ul> </div> </div> </div> </main> ); } } export default App;
此代码包括四个项目的一些硬编码值。 这些将是临时值,直到从后端获取项目。
renderTabList()
函数呈现两个跨度,帮助控制显示哪一组项目。 点击 Completed 选项卡将显示已完成的任务。 单击 Incomplete 选项卡将显示未完成的任务。
保存更改并在 Web 浏览器中观察应用程序:
要处理诸如添加和编辑任务之类的操作,您需要创建一个模态组件。
首先,在src
目录下创建一个components
文件夹:
mkdir src/components
然后,创建一个 Modal.js
文件并使用您的代码编辑器打开它。 添加以下代码行:
前端/src/components/Modal.js
import React, { Component } from "react"; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Input, Label, } from "reactstrap"; export default class CustomModal extends Component { constructor(props) { super(props); this.state = { activeItem: this.props.activeItem, }; } handleChange = (e) => { let { name, value } = e.target; if (e.target.type === "checkbox") { value = e.target.checked; } const activeItem = { ...this.state.activeItem, [name]: value }; this.setState({ activeItem }); }; render() { const { toggle, onSave } = this.props; return ( <Modal isOpen={true} toggle={toggle}> <ModalHeader toggle={toggle}>Todo Item</ModalHeader> <ModalBody> <Form> <FormGroup> <Label for="todo-title">Title</Label> <Input type="text" id="todo-title" name="title" value={this.state.activeItem.title} onChange={this.handleChange} placeholder="Enter Todo Title" /> </FormGroup> <FormGroup> <Label for="todo-description">Description</Label> <Input type="text" id="todo-description" name="description" value={this.state.activeItem.description} onChange={this.handleChange} placeholder="Enter Todo description" /> </FormGroup> <FormGroup check> <Label check> <Input type="checkbox" name="completed" checked={this.state.activeItem.completed} onChange={this.handleChange} /> Completed </Label> </FormGroup> </Form> </ModalBody> <ModalFooter> <Button color="success" onClick={() => onSave(this.state.activeItem)} > Save </Button> </ModalFooter> </Modal> ); } }
这段代码创建了一个 CustomModal
类,它嵌套了从 reactstrap
库派生的 Modal 组件。
这段代码还在表单中定义了三个字段:
title
description
completed
这些字段与我们在后端的 Todo 模型上定义为属性的字段相同。
CustomModal
接收 activeItem
、toggle
和 onSave
作为道具:
activeItem
表示要编辑的 Todo 项。toggle
是用来控制Modal的状态(即打开或关闭modal)的函数。onSave
是调用来保存待办事项的编辑值的函数。
接下来,您将 CustomModal
组件导入到 App.js
文件中。
使用您的代码编辑器重新访问 src/App.js
文件,并将整个内容替换为以下代码行:
前端/src/App.js
import React, { Component } from "react"; import Modal from "./components/Modal"; const todoItems = [ { id: 1, title: "Go to Market", description: "Buy ingredients to prepare dinner", completed: true, }, { id: 2, title: "Study", description: "Read Algebra and History textbook for the upcoming test", completed: false, }, { id: 3, title: "Sammy's books", description: "Go to library to return Sammy's books", completed: true, }, { id: 4, title: "Article", description: "Write article on how to use Django with React", completed: false, }, ]; class App extends Component { constructor(props) { super(props); this.state = { viewCompleted: false, todoList: todoItems, modal: false, activeItem: { title: "", description: "", completed: false, }, }; } toggle = () => { this.setState({ modal: !this.state.modal }); }; handleSubmit = (item) => { this.toggle(); alert("save" + JSON.stringify(item)); }; handleDelete = (item) => { alert("delete" + JSON.stringify(item)); }; createItem = () => { const item = { title: "", description: "", completed: false }; this.setState({ activeItem: item, modal: !this.state.modal }); }; editItem = (item) => { this.setState({ activeItem: item, modal: !this.state.modal }); }; displayCompleted = (status) => { if (status) { return this.setState({ viewCompleted: true }); } return this.setState({ viewCompleted: false }); }; renderTabList = () => { return ( <div className="nav nav-tabs"> <span className={this.state.viewCompleted ? "nav-link active" : "nav-link"} onClick={() => this.displayCompleted(true)} > Complete </span> <span className={this.state.viewCompleted ? "nav-link" : "nav-link active"} onClick={() => this.displayCompleted(false)} > Incomplete </span> </div> ); }; renderItems = () => { const { viewCompleted } = this.state; const newItems = this.state.todoList.filter( (item) => item.completed === viewCompleted ); return newItems.map((item) => ( <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center" > <span className={`todo-title mr-2 ${ this.state.viewCompleted ? "completed-todo" : "" }`} title={item.description} > {item.title} </span> <span> <button className="btn btn-secondary mr-2" onClick={() => this.editItem(item)} > Edit </button> <button className="btn btn-danger" onClick={() => this.handleDelete(item)} > Delete </button> </span> </li> )); }; render() { return ( <main className="container"> <h1 className="text-white text-uppercase text-center my-4">Todo app</h1> <div className="row"> <div className="col-md-6 col-sm-10 mx-auto p-0"> <div className="card p-3"> <div className="mb-4"> <button className="btn btn-primary" onClick={this.createItem} > Add task </button> </div> {this.renderTabList()} <ul className="list-group list-group-flush border-top-0"> {this.renderItems()} </ul> </div> </div> </div> {this.state.modal ? ( <Modal activeItem={this.state.activeItem} toggle={this.toggle} onSave={this.handleSubmit} /> ) : null} </main> ); } } export default App;
保存更改并在 Web 浏览器中观察应用程序:
如果您尝试编辑和保存 Todo
项目,您将收到显示 Todo
项目对象的警报。 单击保存或删除将对Todo
项执行相应的操作。
注意:: 根据您的 React 和 Reactstrap 版本,您可能会遇到控制台错误。 在修订时,Warning: Legacy context API has been detected within a strict-mode tree.
和 Warning: findDOMNode is deprecated in StrictMode.
是 已知 问题 。
现在,您将修改应用程序,使其与您在上一节中构建的 Django API 交互。 重新访问第一个终端窗口并确保服务器正在运行。 如果它没有运行,请使用以下命令:
python manage.py runserver
注意: 如果您关闭了此终端窗口,请记住您需要导航到 backend
目录并使用虚拟 Pipenv shell。
要向后端服务器上的 API 端点发出请求,您将安装一个名为 axios
的 JavaScript 库。
在第二个终端窗口中,确保您位于 frontend
目录并安装 axios
:
npm install axios@0.21.1
然后在代码编辑器中打开 frontend/package.json
文件并添加 proxy
:
前端/package.json
[...] "name": "frontend", "version": "0.1.0", "private": true, "proxy": "http://localhost:8000", "dependencies": { "axios": "^0.18.0", "bootstrap": "^4.1.3", "react": "^16.5.2", "react-dom": "^16.5.2", "react-scripts": "2.0.5", "reactstrap": "^6.5.0" }, [...]
代理将帮助将 API 请求通过隧道传送到 Django 应用程序将处理它们的 http://localhost:8000
。 如果没有这个 proxy
,您将需要指定完整路径:
axios.get("http://localhost:8000/api/todos/")
使用 proxy
,您可以提供相对路径:
axios.get("/api/todos/")
注意: 您可能需要重新启动开发服务器才能让代理注册到应用程序。
重新访问 frontend/src/App.js
文件并使用代码编辑器打开它。 在此步骤中,您将删除硬编码的 todoItems
并使用来自对后端服务器的请求的数据。 handleSubmit
和 handleDelete
打开 App.js
文件并用这个最终版本替换它:
前端/src/App.js
import React, { Component } from "react"; import Modal from "./components/Modal"; import axios from "axios"; class App extends Component { constructor(props) { super(props); this.state = { viewCompleted: false, todoList: [], modal: false, activeItem: { title: "", description: "", completed: false, }, }; } componentDidMount() { this.refreshList(); } refreshList = () => { axios .get("/api/todos/") .then((res) => this.setState({ todoList: res.data })) .catch((err) => console.log(err)); }; toggle = () => { this.setState({ modal: !this.state.modal }); }; handleSubmit = (item) => { this.toggle(); if (item.id) { axios .put(`/api/todos/${item.id}/`, item) .then((res) => this.refreshList()); return; } axios .post("/api/todos/", item) .then((res) => this.refreshList()); }; handleDelete = (item) => { axios .delete(`/api/todos/${item.id}/`) .then((res) => this.refreshList()); }; createItem = () => { const item = { title: "", description: "", completed: false }; this.setState({ activeItem: item, modal: !this.state.modal }); }; editItem = (item) => { this.setState({ activeItem: item, modal: !this.state.modal }); }; displayCompleted = (status) => { if (status) { return this.setState({ viewCompleted: true }); } return this.setState({ viewCompleted: false }); }; renderTabList = () => { return ( <div className="nav nav-tabs"> <span onClick={() => this.displayCompleted(true)} className={this.state.viewCompleted ? "nav-link active" : "nav-link"} > Complete </span> <span onClick={() => this.displayCompleted(false)} className={this.state.viewCompleted ? "nav-link" : "nav-link active"} > Incomplete </span> </div> ); }; renderItems = () => { const { viewCompleted } = this.state; const newItems = this.state.todoList.filter( (item) => item.completed === viewCompleted ); return newItems.map((item) => ( <li key={item.id} className="list-group-item d-flex justify-content-between align-items-center" > <span className={`todo-title mr-2 ${ this.state.viewCompleted ? "completed-todo" : "" }`} title={item.description} > {item.title} </span> <span> <button className="btn btn-secondary mr-2" onClick={() => this.editItem(item)} > Edit </button> <button className="btn btn-danger" onClick={() => this.handleDelete(item)} > Delete </button> </span> </li> )); }; render() { return ( <main className="container"> <h1 className="text-white text-uppercase text-center my-4">Todo app</h1> <div className="row"> <div className="col-md-6 col-sm-10 mx-auto p-0"> <div className="card p-3"> <div className="mb-4"> <button className="btn btn-primary" onClick={this.createItem} > Add task </button> </div> {this.renderTabList()} <ul className="list-group list-group-flush border-top-0"> {this.renderItems()} </ul> </div> </div> </div> {this.state.modal ? ( <Modal activeItem={this.state.activeItem} toggle={this.toggle} onSave={this.handleSubmit} /> ) : null} </main> ); } } export default App;
refreshList()
函数是可重复使用的,每次完成 API 请求时都会调用该函数。 它更新待办事项列表以显示最新添加的项目列表。
handleSubmit()
函数负责创建和更新操作。 如果作为参数传递的项目没有 id
,那么它可能还没有被创建,所以函数创建它。
此时,验证您的后端服务器是否正在您的第一个终端窗口中运行:
python manage.py runserver
注意: 如果您关闭了此终端窗口,请记住您需要导航到 backend
目录并使用虚拟 Pipenv shell。
在您的第二个终端窗口中,确保您位于 frontend
目录中并启动您的前端应用程序:
npm start
现在,当您使用 Web 浏览器访问 http://localhost:3000
时,您的应用程序将允许您执行 READ
、CREATE
、UPDATE
和 DELETE
任务.
这样就完成了 Todo 应用程序的前端和后端。
结论
在本文中,您使用 Django 和 React 构建了一个待办事项应用程序。 您通过 djangorestframework
、django-cors-headers
、axios
、bootstrap
和 reactstrap
库实现了这一点。
如果您想了解有关 Django 的更多信息,请查看 我们的 Django 主题页面 以获取练习和编程项目。
如果您想了解有关 React 的更多信息,请查看我们的 如何在 React.js 中编码,或查看 我们的 React 主题页面 以了解练习和编程项目。