带你进入异步Django+Vue的世界 - Didi打车实战(1)

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: 需要源码的请留言

界面功能介绍:

image.png
  • 乘客界面:
    • 提出行程需求(下单)
    • 查看订单的状态
    • 查看历史行程
image.png
  • 司机界面:
    • 查看所有当前可以接的单子
    • 可以接单
    • 更新订单状态

0. 技术准备

注意:本系列是中高级教程,不是入门教程。
所以请在开始前,自行学习以下入门知识(有基本概念即可,具体细节可以用到时回过头来查询):

一、Django搭建后台框架

不要重复造轮子,这里有Best Practice的脚手架:django-vue-template

  1. 直接克隆到本地:
$ 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
  1. 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
  1. 验证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,应该能看到登录页面:

image.png

  1. build前端文件(可选)
    yarn install
    yarn build

二、鉴权

DRF提供了很多鉴权方式,由于后续要使用Websockets通讯,而Websockets默认是继承HTTP的Cookie(session)鉴权的,所以选择Session。

  1. 在设置里,使用自定义User模型,并指明Session鉴权
# /backend/settings/dev.py 

AUTH_USER_MODEL = 'api.User'

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
            'rest_framework.authentication.SessionAuthentication',
        )
}
  1. 把模型登记到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):
    ...
  1. 数据库迁移
(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

  1. 我们先来创建一个需要登录的视图: 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,需要登录的提示:

    image.png

  • 从admin页面登录,然后在同一浏览器中,再进入http://localhost:8000/api/log_out/。此时看到允许POST的提示。
    (GET方法在LogOutView里没定义,所以405不允许)

image.png
  • 由此证明,Session鉴权已经配置成功

三、用户注册、登录

LogOutView,分别来创建SignUpViewLogInView

  • 注册使用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)
部署到服务器

最终章,如果大家感兴趣,请留言,会放上源码~~