kivantium活動日記

プログラムを使っていろいろやります

Django ChannelsでWebsocket通信を行ってVue.jsで表示する

前回の続きです。 kivantium.hateblo.jp

サーバー上で重たい処理をする場合、処理が全て終わってからHTMLを生成しているとかなりレスポンスが悪くなってしまいます。先に結果表示ページのHTMLを表示しておいて、中身を後からWebsocketで順次通信するといい感じになりそうです。Websocket通信したデータを表示するためにVue.jsを使うことにします。

前回からの変更点

前回のHello, worldで使ったディレクトリ構成を編集する形で作業を進めます。

mysite/routing.py を以下の内容で書き換えます。ws/test/にアクセスすることでWebsocket通信を行えるように設定しています。

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path

from . import consumers

websocket_urlpatterns = [
    path('ws/test/', consumers.Consumer),
]

application = ProtocolTypeRouter({
    # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            websocket_urlpatterns
        )
    ),
})

mysite/consumers.py にWebsocket通信で行う処理を記述します。本当は非同期処理をするべきらしいのですが、動く書き方が分からなかったので同期処理で書いています。(参考: django で Websocket - 空のブログ

import json
import time
import threading

from channels.generic.websocket import WebsocketConsumer

class Consumer(WebsocketConsumer):
    # 接続されたときの処理
    def connect(self):
        # 接続を許可する
        self.accept()
        # メッセージ送信関数を新しいスレッドで呼び出す
        # 本当は非同期で書くべきだがうまく動かなかった
        self.sending = True
        self.sender = threading.Thread(
            target=self.send_message, args=('Hello', ))
        self.sender.start()

    # 接続が切断されたときの処理
    def disconnect(self, close_code):
        # スレッドを終了するフラグを立てる
        self.sending = False
        # スレッドの終了を待つ
        self.sender.join()

    # メッセージを受け取ったときの処理
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        print("Received message:", message)

    # メッセージ送信関数
    def send_message(self, message):
        while True:
            # 終了フラグが立っていたら終了する
            if not self.sending:
                break
            self.send(text_data=json.dumps({
                'message': message,
            }))
            time.sleep(1)

mysite/templates/mysite/index.html にこれと通信するためのHTMLとJavaScriptを書きます。

<!doctype html>
<html>
  <head>
    <title>test</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <ul >
        <li v-for="message in messages">[[ message ]]</li>
      </ul>
    </div>
    <script>
      var vm = new Vue({
        el: '#app',
        // Vueの記号がDjangoと被らないようにする
        delimiters: ['[[', ']]'],
        data: {
          messages: [],
          ws: new WebSocket(
            // http or https の判定
            // https://www.koatech.info/blog/vue-websocket-sample/
            (window.location.protocol == "https:" ? "wss" : "ws")
            + '://' + window.location.host + '/ws/test/'
          ),
        },
        created: function() {
          // Vueオブジェクトにアクセスするために変数化する
          // https://www.koatech.info/blog/vue-websocket-sample/
          const self = this;

          // メッセージを受け取ったときの処理
          self.ws.onmessage = function(e) {
            const data = JSON.parse(e.data);
            console.log(data.message)
            console.log(vm.messages)
            // メッセージを表示する
            vm.messages.push(data.message);
            // サーバーにメッセージを送る
            self.ws.send(JSON.stringify({
              'message': "Konnichiwa",
            }));
          };

          // 接続が切断されたときの処理
          self.ws.onclose = function(e) {
            console.error('Websocket has been closed unexpectedly');
          };
        }
      })
    </script>
  </body>
</html>

mysite/views.py を次のように書き換えてindex.htmlを表示するようにします。

from django.shortcuts import render

def index(request):
    return render(request, 'mysite/index.html')

最後に、テンプレートを認識させるために mysite/settings.pyINSTALLED_APPSmysite を追加します。

INSTALLED_APPS = [
    ......
    'channels',
    'mysite',
]

以上の変更を行ったものをデプロイしてブラウザで閲覧すると1秒ごとにHelloが追加される様子が確認できます。

f:id:kivantium:20200502150709g:plain
スクリーンショット

自分の目的にはこれで十分でしたが、チャットなどのより高度なことをしたい場合はChennelsのドキュメントを読んで下さい。 channels.readthedocs.io

広告コーナー