Python
Django
websocket
React
redux

Django+cannels と React+Redux で Websoket を利用した簡単なチャットっぽいものを作ってみる

(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_APPScore 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

chat_sample.gif

ファイル構成

$ 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 の場合はどう書くのがいいのかな?

参考