上一篇: 帶你進(jìn)入異步Django+Vue的世界 - Didi打車實(shí)戰(zhàn)(3)
后臺數(shù)據(jù)模型設(shè)計
Demo: https://didi-taxi.herokuapp.com/
Django Channels
為了支持即時消息收發(fā)、群發(fā)群收稍浆、異步處理族铆,我們對后臺添加Channels。
安裝Channels:
pip install channels channels-redis
修改配置文件:
- 添加
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]},
}
}
- 創(chuàng)建ASGI application
運(yùn)行django時,會切換到ASGI服務(wù)模式驶睦,同時處理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()
- 創(chuàng)建ASGI路由文件
前端以/ws/taxi/
地址來進(jìn)行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),
])
)
})
- 創(chuàng)建Consumer
Consumer是實(shí)現(xiàn)類似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)
拒絕非登錄用戶。對于已登錄用戶溉痢,則接收連接僻造,并返回歡迎信息。
目錄改動不少孩饼,當(dāng)前結(jié)構(gòu)為:
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
運(yùn)行ASGI
啟動Redis服務(wù):redis-server&
運(yùn)行Django: python manage.py runserver
看到如下提示髓削,就說明ASGI服務(wù)成功啟動了:
(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一樣宝泵,方便復(fù)用。
# /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
}
測試環(huán)境下闯捎,Vue需要代理轉(zhuǎn)發(fā)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')
}
},
用戶退出時瓤鼻,關(guān)閉WS
signUserOut 時,調(diào)用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娱仔,關(guān)閉websockets連接
const mutations = {
...
closeWS (state) {
state.websocket.status = 'CLOSED'
state.websocket.ws.close()
state.websocket.ws = null
}
}
同時,在Django的服務(wù)器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]
總結(jié)
后臺添加了Channels牲迫,支持Websockets。
前端也搭建好了Websockets的框架借卧,方便地連接盹憎、發(fā)送、接收和斷開铐刘。
下一篇陪每,會介紹群發(fā)、群收功能
帶你進(jìn)入異步Django+Vue的世界 - Didi打車實(shí)戰(zhàn)(5)