如何使用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 模型的三个属性:
titledescriptioncompleted
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 组件。
这段代码还在表单中定义了三个字段:
titledescriptioncompleted
这些字段与我们在后端的 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 主题页面 以了解练习和编程项目。