(Django2.0.1, Python3.6.4, node8.9.0)
Django+cannels でサーバ作成
channelsまわりはほぼ 公式の Getting Started with Channels なので詳細知りたい場合はそちらを参照してください。
Python ライブラリインストール
$ pip install django
$ pip install channels
Djangoプロジェクト作成
ついでに startapp も
$ django-admin startproject core
$ mv core sample_chat
$ cd sample_chat
$ python manage.py startapp chat
settings.py
INSTALLED_APPS
に core
chat
channels
追加と CHANNEL_LAYERSを追加
core/settings.py
INSTALLED_APPS = [
# ...
'core',
'chat',
'channels',
]
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "chat.routing.channel_routing",
},
}
chat アプリのソースコード
chat/consumers.py
def http_consumer(message):
# Make standard HTTP response - access ASGI path attribute directly
response = HttpResponse("Hello world! You asked for %s" % message.content['path'])
# Encode that response into message format (ASGI)
for chunk in AsgiHandler.encode_response(response):
message.reply_channel.send(chunk)
chat/routing.py
from channels.routing import route
channel_routing = [
route("http.request", "chat.consumers.http_consumer"),
]
chat/consumers.py
from channels import Group
# Connected to websocket.connect
def ws_add(message):
# Accept the connection
message.reply_channel.send({"accept": True})
# Add to the chat group
Group("chat").add(message.reply_channel)
# Connected to websocket.receive
def ws_message(message):
Group("chat").send({
"text": message.content["text"],
})
# Connected to websocket.disconnect
def ws_disconnect(message):
Group("chat").discard(message.reply_channel)
chat/routing.py
from channels.routing import route
from .consumers import ws_add, ws_message, ws_disconnect
channel_routing = [
route("websocket.connect", ws_add),
route("websocket.receive", ws_message),
route("websocket.disconnect", ws_disconnect),
]
動作確認
開発サーバ起動
$ python manage.py runserver
ブラウザで複数タブで表示させて chrome dev tools のコンソールから下記JSを各タブで実行
// Note that the path doesn't matter right now; any WebSocket
// connection gets bumped over to WebSocket consumers
socket = new WebSocket("ws://" + window.location.host + "/chat/");
socket.onmessage = function(e) {
alert(e.data);
}
socket.onopen = function() {
socket.send("hello world");
}
// Call onopen directly if socket is already open
if (socket.readyState == WebSocket.OPEN) socket.onopen();
複数のタブで hello world
がアラート表示される
React+Reduxでフロント作成
Reactプロジェクト作成とライブラリをインストール
$ npx create-react-app my-app
$ cd my-app
$ npm install redux react-redux redux-thunk --save
ソースコード
src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import thunkMiddleware from 'redux-thunk'
import { createStore, applyMiddleware } from 'redux'
import Messages from './containers/Messages'
import reducer from './reducers'
import registerServiceWorker from './registerServiceWorker'
export const store = createStore(
reducer,
applyMiddleware(thunkMiddleware),
)
ReactDOM.render(
<Provider store={store}>
<Messages />
</Provider>,
document.getElementById('root')
)
registerServiceWorker()
src/reducers/index.js
import { combineReducers } from 'redux'
import { message } from './message'
const reducer = combineReducers({
message,
})
export default reducer
src/message.js
export const message = (state={list: []}, action) => {
switch (action.type) {
case 'CREATE_MESSAGE':
return Object.assign({}, state, {list: [...state.list, action.payload]})
default:
return state
}
}
django の manage.py runserver
のデフォルトが localhost:8000
なのでこちらも同じく
src/actions/messages.js
const WS_URL = 'ws://localhost:8000/chat/'
let ws
export const initWebsocket = () => {
return (dispatch) => {
dispatch(connectToWebsocket())
}
}
export const closeWebsocket = () => {
return () => ws.close()
}
export const createMessage = (text) => {
return () => {
const message = {
text: text,
}
ws.send(JSON.stringify(message))
}
}
export const connectToWebsocket = () => {
ws = new WebSocket(WS_URL)
return dispatch => {
ws.onmessage = (message) => {
dispatch({
type: 'CREATE_MESSAGE',
payload: JSON.parse(message.data)
})
}
}
}
src/containers/MessageForm.js
import React from 'react'
import {connect} from 'react-redux'
import {createMessage} from '../actions/messages'
let MessageForm = ({ dispatch }) => {
let input
const sendMessage = (input) => {
const message = input.value.trim()
if (!message) return
dispatch(createMessage(message))
input.value = ''
}
return (
<form onSubmit={(e) => {
e.preventDefault()
sendMessage(input)
}}>
<input ref={node => {input = node}}/> {/* assign the node reference to the input variable */}
</form>
)
}
export default connect()(MessageForm)
src/containers/Messages.js
import React from 'react'
import { connect } from 'react-redux'
import MessageForm from './MessageForm'
import { initWebsocket, closeWebsocket } from '../actions/messages'
const Message = ({ text }) => (
<li>
{ text }
</li>
)
class Messages extends React.Component {
componentDidMount() {
document.title = 'Chat Room'
const { initWebsocket } = this.props
initWebsocket()
}
componentWillUnmount() {
const { closeWebsocket } = this.props
closeWebsocket()
}
render() {
const messages = (this.props.message.list.slice().reverse().map((message, i) => <Message key={ i } { ...message }/>))
return (
<div>
<MessageForm/>
<ul>
{ messages }
</ul>
</div>
)
}
}
export default connect(
state => state,
dispatch => ({
initWebsocket: () => dispatch(initWebsocket()),
closeWebsocket: () => dispatch(closeWebsocket()),
}),
)(Messages)
不要なファイルを削除
- src/App.css
- src/App.js
- src/App.test.js
- src/index.css
動作確認
フロントの開発サーバを起動
$ npm start
ファイル構成
$ tree . -I node_modules
.
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src
├── actions
│ └── messages.js
├── containers
│ ├── MessageForm.js
│ └── Messages.js
├── index.js
├── reducers
│ ├── index.js
│ └── message.js
└── registerServiceWorker.js
感想
- Django+cannels わりとシンプルに使えてよさそう。パフォーマンスを求めなければw
- React+Redux で websocket 使えるライブラリ が結構な数あって調べるのが面倒。結局それらのライブラリ使わなくてもとりあえずできた。
- Vue.js の場合はどう書くのがいいのかな?