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