LoginSignup
30
37

More than 5 years have passed since last update.

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

Posted at

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

参考

30
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
37