DRF + react 实现TodoList

在web项目构建中有很多框架可供选择,开发人员对项目的使用选择,有很多的影响因素,其中之一就是框架在定义该项目的单独任务时的复杂性。

简介

本文有如下几个部分:

  • 准备
  • 配置后端
  • 配置APIs
  • 配置前端
  • 测试

使用Django和React 编写Todolist程序有如下原因:

  • React框架有广泛的适用范围,遇到错误的问题,可以很快面向Google解决
  • Django 是一个很强大的web框架,从会话到身份认证的功能实现将会节约大量的时间

Django和React配合将可以方便的实现应用程序编写,为了使前后端交互(即检索和存储),为了创建交互界面,我们将使用Django REST框架(DRF)在后端构建API(应用程序编程接口)。

最后实现的TodoList效果如下:

 准备

本次环境如下:

1 Ubuntu 18.4
2 python 3.6.8
3 pip
4 pipenv

之前一直是用的pip + 手动创建虚拟环境。pipenv 是生产就绪的工具,正如其名,他将pip和virtualenv整合在一起

配置 backend

按照如下步骤将建立一个比较好的项目目录

1 mkdir django-todo-react
2 cd django-todo-react

使用pip安装pipenv,并激活一个虚拟环境

1 pip install pipenv
2 pipenv shell

(配置国内镜像跟新文件和pip源,清华,以提高依赖安装速度)
使用pipenv安装django,并新修建一个项目叫backend

1 pipenv install django
2 django-admin startproject backend

 进入backend文件夹 ,新创建一个todo的应用,并迁移数据库

(如果你的机器里有python2.x和python3.x在后面使用python,pip命令时请分别使用python3,pip3)

1 cd backend
2 python3 manage.py startapp todo
3 python3 manage.py migrate
4 python3 manage.py runserver

如上步骤输入都正确的话,输入地址http://127.0.0.1:8000
会出现下图

好了现在开始配置Todo应用

注册应用Todo

在上面已经完成了后端的基苯配置,现在可以将todo应用程序注册为已安装的应用程序,以便Django识别。打开/backend/settings.py文件,更新INSTALLED_APPS

 1 # backend/settings.py
 2 
 3 # Application definition
 4 INSTALLED_APPS = [
 5 'django.contrib.admin',
 6 'django.contrib.auth',
 7 'django.contrib.contenttypes',
 8 'django.contrib.sessions',
 9 'django.contrib.messages',
10 'django.contrib.staticfiles',
11 'todo' # 此处
12 ]
定义 Todo model

打开文件todo/model.py,编写模型

 1 # todo/models.py
 2 
 3 from django.db import models
 4 # Create your models here.
 5 
 6 # add this
 7 class Todo(models.Model):
 8     title = models.CharField(max_length=120)
 9     description = models.TextField()
10     completed = models.BooleanField(default=False)
11 
12     def _str_(self):
13         return self.title

在以上Todo类中描述了三个属性:

  • Title
  • Description
  • Completed

由于改变了Todo model,现在需要对数据做一个迁移,以确保数据库改变。

1 python3 manage.py makemigrations todo
2 python manage.py migrate todo

对todo应用进行配置,修改文件todo/admin.py

 1 # todo/admin.py
 2 
 3 from django.contrib import admin
 4 from .models import Todo # add this
 5 
 6 class TodoAdmin(admin.ModelAdmin): # add this
 7     list_display = ('title', 'description', 'completed') # add this
 8 
 9 # Register your models here.
10     admin.site.register(Todo, TodoAdmin) # add this

创建账户

1 python3 manage.py createsuperuser

启动服务器,访问地址-172.0.0.1:8000/admin

1 python3 manage.py runserver

登陆后,如下图所示,

 

进入添加如下:

配置 APIs

使用 ctr + c 停止服务器,然后使用pipenv安装djangorestframework 和django-cors-headers

1 pipenv install djangorestframework django-cors-headers

现在需要把rest_framework和corsheaders 注册,

 1 INSTALLED_APPS = [
 2     'django.contrib.admin',
 3     'django.contrib.auth',
 4     'django.contrib.contenttypes',
 5     'django.contrib.sessions',
 6     'django.contrib.messages',
 7     'django.contrib.staticfiles',
 8     'corsheaders', # add this
 9     'rest_framework', # add this 
10     'todo',
11 ]
12 MIDDLEWARE = [
13     'corsheaders.middleware.CorsMiddleware', # add this
14     'django.middleware.common.CommonMiddleware', # add this
15     'django.middleware.security.SecurityMiddleware',
16     'django.contrib.sessions.middleware.SessionMiddleware',
17     'django.middleware.common.CommonMiddleware',
18     'django.middleware.csrf.CsrfViewMiddleware',
19     'django.contrib.auth.middleware.AuthenticationMiddleware',
20     'django.contrib.messages.middleware.MessageMiddleware',
21     'django.middleware.clickjacking.XFrameOptionsMiddleware',
22 ]
23 CORS_ALLOW_CREDENTIALS = True # add this
24 CORS_ORIGIN_ALLOW_ALL = True # add this

网上教程在backend/settings.py最后添加以下,一直报错,后来只有不添加如下即可运行:

1 # we whitelist localhost:3000 because that's where frontend will be served
2 CORS_ORIGIN_WHITELIST = (
3     'localhost:3000/'
4 )

在CORS_ORIGIN_WHITELIST片段中,我们将localhost:3000列入白名单,因为我们希望应用程序的前端(将在该端口上提供)与API进行交互

Todo model 序列化

序列化相关参见此文

我们需要序列化程序将模型实例转换为JSON,以便前端可以轻松处理接收到的数据。
新建文件todo/serializers.py

1 touch todo/serializers.py

编辑文件像如下代码:

1 # todo/serializers.py
2 
3 from rest_framework import serializers
4 from .models import Todo
5 
6 class TodoSerializer(serializers.ModelSerializer):
7     class Meta:
8       model = Todo
9       fields = ('id', 'title', 'description', 'completed')

在上面代码片段中,我们指定了要使用的模型以及我们要转换为JSON的字段。

编写View视图

在todo/views.py新建Todoview类,

 1 # todo/views.py
 2 
 3 from django.shortcuts import render
 4 from rest_framework import viewsets # add this
 5 from .serializers import TodoSerializer # add this
 6 from .models import Todo # add this
 7 
 8 class TodoView(viewsets.ModelViewSet): # add this
 9     serializer_class = TodoSerializer # add this
10     queryset = Todo.objects.all() # add this

视图集基类默认提供CRUD操作的实现,我们要做的是指定序列化程序类和查询集。
编写后端backend/urls.py文件

 1 # backend/urls.py
 2 
 3 from django.contrib import admin
 4 from django.urls import path, include # add this
 5 from rest_framework import routers # add this
 6 from todo import views # add this
 7 
 8 router = routers.DefaultRouter() # add this
 9 router.register(r'todos', views.TodoView, 'todo') # add this
10 
11 urlpatterns = [
12     path('admin/', admin.site.urls), 
13     path('api/', include(router.urls)) # add this
14 ]

这是完成API构建的最后一步,我们现在可以在Todo模型上执行CRUD操作。路由器类允许我们进行以下查询:

  • /todos/ 这将返回所有Todo项的列表(可以在此处完成Create和Read操作)。
  • /todos/id 这将使用id主键返回单个Todo项(可以在此处执行Update和Delete操作)。

运行下列地址访问服务器127.0.0.1:8000/api/todos

1 python3 manage.py runserver

如下图所示:

 

可以新建TodoList,以测试:

配置前端

在一个新的命令行终端上执行命令

1 cd django-todo-react
2 npm install -g create-react-app
3 create-react-app frontend
4 cd frontend 
5 yarn start

访问127.0.0.1:3000将会看见如下:

引入bootstrap和reactstrap来增加UI的功能:

1 yarn add bootstrap reactstrap

编辑文件src/index.css

 1 /__ frontend/src/index.css __/
 2 body {
 3   margin: 0;
 4   padding: 0;
 5   font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
 6     "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
 7     sans-serif;
 8   -webkit-font-smoothing: antialiased;
 9   -moz-osx-font-smoothing: grayscale;
10   background-color: #282c34;
11 }
12 .todo-title {
13   cursor: pointer;
14 }
15 .completed-todo {
16   text-decoration: line-through;
17 }
18 .tab-list > span {
19   padding: 5px 8px;
20   border: 1px solid #282c34;
21   border-radius: 10px;
22   margin-right: 5px;
23   cursor: pointer;
24 }
25 .tab-list > span.active {
26   background-color: #282c34;
27   color: #ffffff;
28 }

在src/index.js 添加下面引入文件

 1 import React from 'react';
 2 import ReactDOM from 'react-dom';
 3 import 'bootstrap/dist/css/bootstrap.min.css'; //add this
 4 import './index.css';
 5 import App from './App';
 6 import * as serviceWorker from './serviceWorker';
 7 
 8 ReactDOM.render(<App />, document.getElementById('root'));
 9 
10 // If you want your app to work offline and load faster, you can change
11 // unregister() to register() below. Note this comes with some pitfalls.
12 // Learn more about service workers: https://bit.ly/CRA-PWA
13 serviceWorker.unregister();
安装axios
1 yarn add axios

把axios 添加进入package.json文件

 1 // frontend/package.json
 2 
 3 [...] "name": "frontend",
 4 "version": "0.1.0",
 5 "private": true,
 6 "proxy": "http://localhost:8000", // 只需要添加这一行
 7 "dependencies": {
 8   "axios": "^0.18.0",
 9   "bootstrap": "^4.1.3",
10   "react": "^16.5.2",
11   "react-dom": "^16.5.2",
12   "react-scripts": "2.0.5",
13   "reactstrap": "^6.5.0"
14 },
15 [...]

要处理添加和编辑任务等操作,我们将使用模态,所以让我们在components文件夹中创建一个Modal组件。先在src目录下新建文件夹components,

1 mkdir src/components
2 touch src/components/Modal.js

编辑Modal.js文件

 1 // frontend/src/components/Modal.js
 2 import React, { Component } from "react";
 3 import {
 4   Button,
 5   Modal,
 6   ModalHeader,
 7   ModalBody,
 8   ModalFooter,
 9   Form,
10   FormGroup,
11   Input,
12   Label
13 } from "reactstrap";
14 export default class CustomModal extends Component {
15   constructor(props) {
16     super(props);
17     this.state = {
18       activeItem: this.props.activeItem
19     };
20   }
21   handleChange = e => {
22     let { name, value } = e.target;
23     if (e.target.type === "checkbox") {
24       value = e.target.checked;
25     }
26     const activeItem = { ...this.state.activeItem, [name]: value };
27     this.setState({ activeItem });
28   };
29   render() {
30     const { toggle, onSave } = this.props;
31     return (
32       <Modal isOpen={true} toggle={toggle}>
33         <ModalHeader toggle={toggle}> Todo Item </ModalHeader>
34         <ModalBody>
35           <Form>
36             <FormGroup>
37               <Label for="title">Title</Label>
38               <Input
39                 type="text"
40                 name="title"
41                 value={this.state.activeItem.title}
42                 onChange={this.handleChange}
43                 placeholder="Enter Todo Title"
44               />
45             </FormGroup>
46             <FormGroup>
47               <Label for="description">Description</Label>
48               <Input
49                 type="text"
50                 name="description"
51                 value={this.state.activeItem.description}
52                 onChange={this.handleChange}
53                 placeholder="Enter Todo description"
54               />
55             </FormGroup>
56             <FormGroup check>
57               <Label for="completed">
58                 <Input
59                   type="checkbox"
60                   name="completed"
61                   checked={this.state.activeItem.completed}
62                   onChange={this.handleChange}
63                 />
64                 Completed
65               </Label>
66             </FormGroup>
67           </Form>
68         </ModalBody>
69         <ModalFooter>
70           <Button color="success" onClick={() => onSave(this.state.activeItem)}>
71             Save
72           </Button>
73         </ModalFooter>
74       </Modal>
75     );
76   }
77 }

我们创建了一个CustomModal类,它嵌套了从reactstrap库派生的Modal组件。我们还在表单中定义了三个字段

  • Title
  • Description
  • Completed

接下来就是 修改文件src/Appjs

  1 // frontend/src/App.js
  2 import React, { Component } from "react";
  3 import Modal from "./components/Modal";
  4 import axios from "axios";
  5 class App extends Component {
  6   constructor(props) {
  7     super(props);
  8     this.state = {
  9       viewCompleted: false,
 10       activeItem: {
 11         title: "",
 12         description: "",
 13         completed: false
 14       },
 15       todoList: []
 16     };
 17   }
 18   componentDidMount() {
 19     this.refreshList();
 20   }
 21   refreshList = () => {
 22     axios
 23       .get("http://localhost:8000/api/todos/")
 24       .then(res => this.setState({ todoList: res.data }))
 25       .catch(err => console.log(err));
 26   };
 27   displayCompleted = status => {
 28     if (status) {
 29       return this.setState({ viewCompleted: true });
 30     }
 31     return this.setState({ viewCompleted: false });
 32   };
 33   renderTabList = () => {
 34     return (
 35       <div className="my-5 tab-list">
 36         <span
 37           onClick={() => this.displayCompleted(true)}
 38           className={this.state.viewCompleted ? "active" : ""}
 39         >
 40           complete
 41         </span>
 42         <span
 43           onClick={() => this.displayCompleted(false)}
 44           className={this.state.viewCompleted ? "" : "active"}
 45         >
 46           Incomplete
 47         </span>
 48       </div>
 49     );
 50   };
 51   renderItems = () => {
 52     const { viewCompleted } = this.state;
 53     const newItems = this.state.todoList.filter(
 54       item => item.completed === viewCompleted
 55     );
 56     return newItems.map(item => (
 57       <li
 58         key={item.id}
 59         className="list-group-item d-flex justify-content-between align-items-center"
 60       >
 61         <span
 62           className={`todo-title mr-2 ${
 63             this.state.viewCompleted ? "completed-todo" : ""
 64           }`}
 65           title={item.description}
 66         >
 67           {item.title}
 68         </span>
 69         <span>
 70           <button
 71             onClick={() => this.editItem(item)}
 72             className="btn btn-secondary mr-2"
 73           >
 74             {" "}
 75             Edit{" "}
 76           </button>
 77           <button
 78             onClick={() => this.handleDelete(item)}
 79             className="btn btn-danger"
 80           >
 81             Delete{" "}
 82           </button>
 83         </span>
 84       </li>
 85     ));
 86   };
 87   toggle = () => {
 88     this.setState({ modal: !this.state.modal });
 89   };
 90   handleSubmit = item => {
 91     this.toggle();
 92     if (item.id) {
 93       axios
 94         .put(`http://localhost:8000/api/todos/${item.id}/`, item)
 95         .then(res => this.refreshList());
 96       return;
 97     }
 98     axios
 99       .post("http://localhost:8000/api/todos/", item)
100       .then(res => this.refreshList());
101   };
102   handleDelete = item => {
103     axios
104       .delete(`http://localhost:8000/api/todos/${item.id}`)
105       .then(res => this.refreshList());
106   };
107   createItem = () => {
108     const item = { title: "", description: "", completed: false };
109     this.setState({ activeItem: item, modal: !this.state.modal });
110   };
111   editItem = item => {
112     this.setState({ activeItem: item, modal: !this.state.modal });
113   };
114   render() {
115     return (
116       <main className="content">
117         <h1 className="text-white text-uppercase text-center my-4">Todo app</h1>
118         <div className="row ">
119           <div className="col-md-6 col-sm-10 mx-auto p-0">
120             <div className="card p-3">
121               <div className="">
122                 <button onClick={this.createItem} className="btn btn-primary">
123                   Add task
124                 </button>
125               </div>
126               {this.renderTabList()}
127               <ul className="list-group list-group-flush">
128                 {this.renderItems()}
129               </ul>
130             </div>
131           </div>
132         </div>
133         {this.state.modal ? (
134           <Modal
135             activeItem={this.state.activeItem}
136             toggle={this.toggle}
137             onSave={this.handleSubmit}
138           />
139         ) : null}
140       </main>
141     );
142   }
143 }
144 export default App;

这时基本的代码已经写完,可以进行测试了

测试

分别在刚才两个命令行运行APIs端和前端

1 # django-todo-react/backend
2 python3 manage.py runserver # 运行DRF
1 yarn start # 运行react

最后访问 127.0.0.1:3000/

 


访问127.0.0.1:8000/api

参考

原作者个人感觉写的很详细了

原文地址:https://www.cnblogs.com/Rightsec/p/11182705.html