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

(Django2.0.1, Python3.6.4, node8.9.0)

## Django+cannels でサーバ作成

channelsまわりはほぼ [公式の Getting Started with Channels](https://channels.readthedocs.io/en/latest/getting-started.html) なので詳細知りたい場合はそちらを参照してください。


### 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
```

- http://localhost:8000

ブラウザで複数タブで表示させて chrome dev tools のコンソールから下記JSを各タブで実行


```javascript
// 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
```

- http://localhost:3000

![chat_sample.gif](https://qiita-image-store.s3.amazonaws.com/0/41656/ca2c391d-f5f1-e420-2a4b-b9fb6612e815.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 使えるライブラリ](https://github.com/markerikson/redux-ecosystem-links/blob/master/middleware-sockets-adapters.md) が結構な数あって調べるのが面倒。結局それらのライブラリ使わなくてもとりあえずできた。
- Vue.js の場合はどう書くのがいいのかな？

## 参考

- [Getting Started with Channels — Channels 1.1.8 documentation](https://channels.readthedocs.io/en/latest/getting-started.html)
- [Django ChannelsでできるリアルタイムWeb - Qiita] (https://qiita.com/massa142/items/cbd508efe0c45b618b34)
- [jacobian/channels-example: An example Channels app  ](https://github.com/jacobian/channels-example/)
- [existing socket-related middleware](https://github.com/markerikson/redux-ecosystem-links/blob/master/middleware-sockets-adapters.md)
- https://github.com/rollokb/redux-ws-chatapp