Django是python里著名的三大Web框架之一(另二个是Flask和Tornado)。
Django原生跟Flask一样,是同步的。
当前对异步处理要求越来越多,Django Channels应运而生,用于在后台处理Http和Websockets请求。
后期Channels计划合并到django发行版本中。
本文后端源码参考:https://testdriven.io/courses/real-time-app-with-django-channels-and-angular/part-one-intro/
关键字:django Vue.js Channels Websockets
本系列通过一个实时打车(类似于Didi)的实战项目,来学习如何综合运用:
- 后端 Django (v2.2)
- Channels (v2.2) : 处理Websockets、异步请求、群发群收
- RESTful: DRF (v3.9.2) : HTTP, Sessioin鉴权
- 前端 Vue.js (v2.6)
- Vuex + Vue-router + axios
- UI: 同时支持桌面端和手机端 Vuetify (v1.5)
- Redis (v4.0)
- Postgresql (v10)
- Python (v3.7)
- 开发环境 Ubuntu 18.04 (Win10用户参考这里)
- 部署到生产环境
Demo: https://didi-taxi.herokuapp.com/
Github: 需要源码的请留言
界面功能介绍:
- 乘客界面:
- 提出行程需求(下单)
- 查看订单的状态
- 查看历史行程
- 司机界面:
- 查看所有当前可以接的单子
- 可以接单
- 更新订单状态
0. 技术准备
注意:本系列是中高级教程,不是入门教程。
所以请在开始前,自行学习以下入门知识(有基本概念即可,具体细节可以用到时回过头来查询):
- Python3: Liaoxuefeng
- Django: http://www.liujiangblog.com/course/django/2
- Django Channels: https://channels.readthedocs.io/en/latest/tutorial/index.html
- DRF (Django REST Framework): https://www.django-rest-framework.org/tutorial/quickstart/
- Vue: https://www.jianshu.com/p/51c8b35e2c9f
- Vuex: https://vuex.vuejs.org/zh/
- Vue-Router: https://router.vuejs.org/zh/
一、Django搭建后台框架
不要重复造轮子,这里有Best Practice的脚手架:django-vue-template
- 直接克隆到本地:
$ mkdir didi-project
$ cd didi-project/
$ git clone https://github.com/kevinqqnj/django-vue-template.git . <=注意有个点
$ ls
LICENSE Pipfile.lock README.md backend package.json src yarn.lock
Pipfile Procfile app.json manage.py public vue.config.js
- Python虚拟环境,使用pipenv
如果没安装,使用pip3 install pipenv
安装
$ pipenv shell
Creating a virtualenv for this project…
Pipfile: /mnt/c/Users/git/didi-project/Pipfile
Using /usr/bin/python3 (3.7.3) to create virtualenv…
⠦ Creating virtual environment...Using base prefix '/usr'
Running virtualenv with interpreter /usr/bin/python3
✔ Successfully created virtual environment!
# 安装python依赖,以及Pillow
(didi-project) /mnt/c/Users/git/didi-project$ pipenv install
(didi-project) /mnt/c/Users/git/didi-project$ pipenv install pillow
- 验证django后台已经正常启动
(didi-project) $ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
May 05, 2019 - 10:20:42
Django version 2.2, using settings 'backend.settings.dev'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
此时,打开浏览器,输入http://localhost:8000/admin
,应该能看到登录页面:
- build前端文件(可选)
yarn install
yarn build
二、鉴权
DRF
提供了很多鉴权方式,由于后续要使用Websockets
通讯,而Websockets默认是继承HTTP的Cookie(session)鉴权的,所以选择Session。
- 在设置里,使用自定义User模型,并指明Session鉴权
# /backend/settings/dev.py
AUTH_USER_MODEL = 'api.User'
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
)
}
- 把模型登记到django admin里:
# /backend/api/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin
from .models import User
@admin.register(User)
class UserAdmin(DefaultUserAdmin):
...
- 数据库迁移
(didi-project) $ python manage.py makemigrations api
Migrations for 'api':
backend/api/migrations/0001_initial.py
- Create model User
(didi-project) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, api, auth, contenttypes, sessions
Running migrations:
...
Applying sessions.0001_initial... OK
创建超级(admin)用户
(didi-project) $ python manage.py createsuperuser
Username: admin
Email address: aaa@bbb.com
Password:
Password (again):
This password is entirely numeric.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
此时,可以用这个账号登录http://localhost:8000/admin
了
- 我们先来创建一个需要登录的视图:
LogOutView
# /backend/api/views.py
from rest_framework import generics, permissions, status, views, viewsets
from rest_framework.response import Response
class LogOutView(views.APIView):
permission_classes = (permissions.IsAuthenticated,)
def post(self, *args, **kwargs):
logout(self.request)
return Response(status=status.HTTP_204_NO_CONTENT)
!!删除:class MessageViewSet(viewsets.ModelViewSet)
添加/api/log_out/
路由
# /backend/urls.py
from .api.views import index_view, serve_worker_view, LogOutView
path('api/log_out/', LogOutView.as_view(), name='log_out'),
!!删除:
router = routers.DefaultRouter()
router.register('messages', MessageViewSet)
path('api/', include(router.urls)),
测试 - Session保护的路由:
-
确保admin页面已经退出登录,然后输入
http://localhost:8000/api/log_out/
。应该看到403,需要登录的提示:
从admin页面登录,然后在同一浏览器中,再进入
http://localhost:8000/api/log_out/
。此时看到允许POST的提示。
(GET方法在LogOutView
里没定义,所以405不允许)
- 由此证明,Session鉴权已经配置成功
三、用户注册、登录
同LogOutView
,分别来创建SignUpView
、LogInView
- 注册使用
CreateAPIView
,需要使用序列化器。
判断两次密码是否一致,然后调用create_user
方法创建用户
# /backend/api/serializers.py
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.conf import settings
from rest_framework import serializers
from urllib.parse import urljoin
class UserSerializer(serializers.ModelSerializer):
password1 = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True)
def validate(self, data):
if data['password1'] != data['password2']:
raise serializers.ValidationError('两次密码不一致')
return data
def create(self, validated_data):
data = {
key: value for key, value in validated_data.items()
if key not in ('password1', 'password2')
}
data['password'] = validated_data['password1']
user = self.Meta.model.objects.create_user(**data)
return user
class Meta:
model = get_user_model()
fields = (
'id', 'username', 'password1', 'password2', 'first_name', 'last_name',
)
read_only_fields = ('id',)
- DRF视图里引用这个序列化器
# /backend/api/views.py
class SignUpView(generics.CreateAPIView):
queryset = get_user_model().objects.all()
serializer_class = UserSerializer
- 添加路由
# /backend/urls.py
from .api.views import index_view, serve_worker_view, SignUpView, LogOutView
path('api/sign_up/', SignUpView.as_view(), name='sign_up'),
测试 - 创建用户:
使用curl
或浏览器http://localhost:8000/api/sign_up/
均可。
(didi-project) $ curl -i -d '{"username":"user1", "password1":"aaa", "password2":"aaa"}' -H 'Content-Type: application/json' localhost:8000/api/sign_up/
HTTP/1.1 201 Created
Date: Sun, 05 May 2019 13:11:25 GMT
Server: WSGIServer/0.2 CPython/3.7.3
Content-Type: application/json
Vary: Accept, Cookie
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 58
{"id":2,"username":"user1","first_name":"","last_name":""}
- 登录视图:
使用AuthenticationForm
表单,来验证登录的数据。并且在返回的Headers
里写入sessioinid
,以便后续的Websockets
鉴权使用
# /backend/api/views.py
class LogInView(views.APIView):
@staticmethod
def post(request):
form = AuthenticationForm(data=request.data)
if form.is_valid():
user = form.get_user()
login(request, user=user)
rsp = UserSerializer(user).data
rsp['sessionid'] = request.session.session_key
return Response(rsp)
else:
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
- 添加路由
# /backend/urls.py
from .api.views import index_view, serve_worker_view, SignUpView, LogInView, LogOutView
path('api/log_in/', LogInView.as_view(), name='log_in'),
测试 - 用户登录:
使用curl
或浏览器http://localhost:8000/api/log_in/
均可。
(didi-project) $ curl -i -d '{"username":"user1", "password":"aaa"}' -H 'Content-Type: application/json' localhost:8000/api/log_in/
HTTP/1.1 200 OK
Date: Sun, 05 May 2019 13:19:30 GMT
Server: WSGIServer/0.2 CPython/3.7.3
Content-Type: application/json
Vary: Accept, Cookie
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 105
Set-Cookie: csrftoken=baEYChsNnKet2RkapIzWsxxxxxxz9xrJUf94Z23ZXoUauxkjq6iEC7Pr2F2; expires=Sun, 03 May 2020 13:19:30 GMT; Max-Age=31449600; Path=/; SameSite=Lax
Set-Cookie: sessionid=rue1qryxj84z77d0azeyo6l61i230u4z; expires=Sun, 19 May 2019 13:19:30 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
{"id":2,"username":"user1","first_name":"","last_name":"","sessionid":"rue1qryxj8xxx7d0azeyoxxxxx30u4z"}
总结:
搭建项目框架,实现后台鉴权系统。
带你进入异步Django+Vue的世界 - Didi打车实战(2) https://www.jianshu.com/p/f6a83315e055
Vue + Vuetify 前端鉴权实现
带你进入异步Django+Vue的世界 - Didi打车实战(3)
Vue websockets实现
带你进入异步Django+Vue的世界 - Didi打车实战(4)
Channels + ASGI服务器
带你进入异步Django+Vue的世界 - Didi打车实战(5)
Websockets通讯 + 群发群收
带你进入异步Django+Vue的世界 - Didi打车实战(6)
Vue群发群收
...
带你进入异步Django+Vue的世界 - Didi打车实战(X)
部署到服务器
最终章,如果大家感兴趣,请留言,会放上源码~~