带你进入异步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)
部署到服务器

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343