帶你進入異步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)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容