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

上一篇: 带你进入异步Django+Vue的世界 - Didi打车实战(3)
后台数据模型设计
Demo: https://didi-taxi.herokuapp.com/

Django Channels

为了支持即时消息收发、群发群收、异步处理,我们对后台添加Channels。

  1. 安装Channels:
    pip install channels channels-redis

  2. 修改配置文件:

  • 添加channels APP
  • 添加ASGI_APPLICATION
  • 添加CHANNEL_LAYERS,使用Redis作为后台
# /backend/settings/dev.py
INSTALLED_APPS = [
    'channels',
。。。

ASGI_APPLICATION = 'backend.routing.application'

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'dist', 'media')

REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379')

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': { 'hosts': [REDIS_URL]},
    }
}
  1. 创建ASGI application
    运行django时,会切换到ASGI服务模式,同时处理HTTP和Websockets访问。
# /backend/asgi.py
import os

import django

from channels.routing import get_default_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings.dev')
django.setup()
application = get_default_application()
  1. 创建ASGI路由文件
    前端以/ws/taxi/地址来进行WebSockets请求。
# backend/routing.py

from django.urls import path 
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter

from .api.consumers import TaxiConsumer

# changed
application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(
        URLRouter([
            path('ws/taxi/', TaxiConsumer),
        ])
    )
})
  1. 创建Consumer
    Consumer是实现类似View的功能,处理WebSockets消息。
# api/consumers.py

from channels.generic.websocket import AsyncJsonWebsocketConsumer

class TaxiConsumer(AsyncJsonWebsocketConsumer):

    async def connect(self):
        user = self.scope['user']
        if user.is_anonymous:
            await self.close()
        else:
            await self.accept()
            content = {
              'type': 'from Django',
              'data': "welcome, you're connected to Channels!"
            }
          await self.send_json(content)

拒绝非登录用户。对于已登录用户,则接收连接,并返回欢迎信息。

目录改动不少,当前结构为:

git/didi-project$ tree -I node_modules -L 3
.
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── Procfile
├── README.md
├── app.json
├── backend
│   ├── __init__.py
│   ├── api
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── consumers.py
│   │   ├── migrations
│   │   ├── models.py
│   │   ├── serializers.py
│   │   ├── tests.py
│   │   ├── urls.py
│   │   └── views.py
│   ├── asgi.py
│   ├── routing.py
│   ├── settings
│   │   ├── __init__.py
│   │   ├── dev.py
│   │   └── prod.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
├── dist
├── manage.py
├── package.json
├── public
│   ├── index.html
│   ├── manifest.json
│   ├── robots.txt
│   └── static
│       ├── favicon.ico
│       └── img
├── src
│   ├── App.vue
│   ├── assets
│   ├── components
│   ├── config.js
│   ├── main.js
│   ├── plugins
│   │   └── vuetify.js
│   ├── registerServiceWorker.js
│   ├── router.js
│   ├── services
│   │   ├── api.js
│   │   └── messageService.js
│   ├── store
│   │   ├── index.js
│   │   └── modules
│   │   │   └── messages.js
│   └── views
│       ├── Home.vue
│       ├── My404.vue
│       ├── Signin.vue
│       └── Signup.vue
├── vue.config.js
└── yarn.lock

运行ASGI

启动Redis服务:redis-server&
运行Django: python manage.py runserver

看到如下提示,就说明ASGI服务成功启动了:

(didi-project) git/didi-project$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
May 19, 2019 - 07:28:17
Django version 2.2.1, using settings 'backend.settings.dev'
Starting ASGI/Channels version 2.2.0 development server at http://127.0.0.1:8000/

验证一下,之前的HTTP登录、注册等功能一切正常。

前端添加WebSockets支持

直接写到<script>里也可以,
const websocket = new WebSocket(...)
但我们可以将公用方法提炼出来,跟axios ajax一样,方便复用。

# /src/service/ws_api.js
// import http from '@/services/http'
import config from '@/config'
// import store from '@/store'

class Ws {
  constructor (path) {
    this.path = path
  }

  init () {
    this.websocket = new WebSocket(`${config.wsUrl}/ws/${this.path}/`)
    return this.websocket
  }
}

export default Ws

Vuex store里,新建一个module - ws.js,专门处理如下actions:

  • initWS
  • wsOnOpen
  • wsOnError
  • wsOnClose
  • wsOnMessage
    在建立、中断WS时,通过setAlert action提示
# /src/store/modules/ws.js
import Ws from '@/services/ws_api.js'

const state = {
  websocket: {
    ws: null,
    status: 'DISCONNECTED',
    content: {},
    code: null
  }
}

const getters = {
  websocket: state => {
    return state.websocket
  }
}

const mutations = {
  initWS (state, ws) {
    state.websocket.status = 'CONNECTING'
    state.websocket.ws = ws
  },
  wsOnOpen (state, path) {
    state.websocket[path] = {}
    state.websocket[path].status = 'CONNECTED'
  },
  wsOnError (state, e) {
    state.websocket.status = 'ERROR'
    state.websocket.code = JSON.stringify(e)
  },
  wsOnClose (state, e) {
    state.websocket.status = 'CLOSED'
    state.websocket.code = JSON.stringify(e)
  },
  wsOnMessage (state, data) {
    // state.websocket.status = 'MESSAGE'
    state.websocket.content = data
  },
  closeWS (state) {
    state.websocket.status = 'CLOSED'
    state.websocket.ws.close()
    state.websocket.ws = null
  }
}

const actions = {
  initWS ({ dispatch, commit }, path) {
    let websocket = new Ws(path)
    const ws = websocket.init()
    ws.onopen = () => dispatch('wsOnOpen', path)
    ws.onerror = (e) => dispatch('wsOnError', e)
    ws.onclose = (e) => dispatch('wsOnClose', e)
    ws.onmessage = (e) => dispatch('wsOnMessage', e)
    commit('initWS', ws)
  },
  async wsOnMessage ({ commit }, e) {
    const rdata = JSON.parse(e.data)
    console.log('WS received: ' + JSON.stringify(rdata))
    // await messageService.wsOnMessage(rdata)
    commit('wsOnMessage', rdata)
  },
  async sendWSMessage ({ commit }, message) {
    let data = JSON.stringify(message)
    await state.websocket.ws.send(data)
  },
  async wsOnOpen ({ commit }, path) {
    commit('wsOnOpen', path)
    let msg = `Websocket(${path}) CONNECTED!`
    commit('setAlert', { type: 'info', msg: msg }, { root: true })
    await console.log(msg)
  },
  async wsOnError ({ commit }, e) {
    commit('wsOnError', e)
    await console.log(`Websocket ERROR: ${e}`)
  },
  async wsOnClose ({ commit }, e) {
    commit('wsOnClose', e)
    commit('setAlert', { type: 'info', msg: `Websocket CLOSED! ${e}` }, { root: true })
    await console.log(`Websocket CLOSED! ${e}`)
  }
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}

测试环境下,Vue需要代理转发WS:

# /vue.config.js
module.exports = {
  outputDir: 'dist',
  assetsDir: 'static',
  devServer: {
    proxy: {
      '/api*': {
        // Forward frontend dev server request for /api to django dev server
        target: 'http://localhost:8000/'
      },
      '/ws/*': {
        // forward websocket
        target: 'ws://localhost:8000/',
        ws: true,
        secure: false,
        logLevel: 'debug'
      }
    }
  }
}

前端测试WebSockets

在用户登录时,连接到WS

在Home.vue里,添加Vuex action

# /src/views/Home.vue
  mounted () {
    if (this.userIsAuthenticated) {
      this.$store.dispatch('messages/getTrips')
      this.$store.dispatch('ws/initWS', 'taxi')
    }
  },
image.png

用户退出时,关闭WS
signUserOut 时,调用ws.js里的mutation

# /src/store/modules/messages.js
  signUserOut ({ commit }) {
    commit('setLoading', true, { root: true })
    messageService.signUserOut()
      .then(messages => {
        commit('ws/closeWS', '', { root: true })
...
      })
  },

Vuex ws.js里,添加一条mutaion,关闭websockets连接

const mutations = {
...
  closeWS (state) {
    state.websocket.status = 'CLOSED'
    state.websocket.ws.close()
    state.websocket.ws = null
  }
}
image.png

同时,在Django的服务器log里,也能看到WebSocket连接/断开的记录:

HTTP POST /api/log_in/ 200 [0.16, 127.0.0.1:49968]
HTTP GET /api/trip/ 200 [0.01, 127.0.0.1:49971]
WebSocket HANDSHAKING /ws/taxi/ [127.0.0.1:49973]
WebSocket CONNECT /ws/taxi/ [127.0.0.1:49973]
HTTP POST /api/log_out/ 204 [0.02, 127.0.0.1:50015]
WebSocket DISCONNECT /ws/taxi/ [127.0.0.1:49973]

总结

后台添加了Channels,支持Websockets。
前端也搭建好了Websockets的框架,方便地连接、发送、接收和断开。

下一篇,会介绍群发、群收功能
带你进入异步Django+Vue的世界 - Didi打车实战(5)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容