LoginSignup
4
6

More than 5 years have passed since last update.

DjangoとVueでカンバンアプリケーションを作る(11)

Last updated at Posted at 2018-10-08

※はじめからはこちら

前回まででカードの並び替えはできるようになりましたが、一瞬カードが戻ってしまう状況だったのでそのあたりを改善していきます。

一瞬もどる原因

前回実装したVuex側のコードで関係するのはこの部分です。

const actions = {
  updateCardOrder({ commit, getters }, { pipeLineId, cardList }) {
    console.log(pipeLineId, cardList);
    const socket = getters.getSocket;
    socket.sendObj({
      type: 'update_card_order',
      pipeLineId,
      cardIdList: cardList.map(x => x.cardId),
    });
  },
};

const mutations = {
  setBoardData(state, { boardData }) {
    state.boardData = camelcaseKeys(boardData, { deep: true });
  },
};

カードの並び順の更新は、以下の流れになります。

  1. ComponentからupdateCardOrder(action)を呼び出し
  2. Websocket経由で情報の更新をサーバにリクエストし
  3. サーバ側がWebsocketで新しいカードの並び順(正確にばボード全体のデータ)を返送し
  4. setBoardData(mutation)が実行される

Component側で、D&Dが完了するのは1.ですが、新しい並び順になるのは4.まで完了した時点です。そのため、その間はD&Dが完了しても、その前のデータがレンダリングされてしまうのでチラツキが発生します。Websocketではサーバが次のメッセージを戻すまで待つといったことができないので、まっとうには避けづらいです。

対策としては、おそらく処理は完了するだろうという楽観的な視点にたって処理を実行したクライアントではサーバのメッセージを待たずにデータを更新してしまうことが有効です。

Storeの変更

まずは、Action/Mutationを変更します。

application/vuejs/src/store/pages/board.js
@@ -29,10 +29,15 @@ const actions = {
       pipeLineId,
       cardIdList: cardList.map(x => x.cardId),
     });
+    commit('updateCardOrder', { pipeLineId, cardList });
   },
 };

 const mutations = {
+  updateCardOrder(state, { pipeLineId, cardList }) { // あるPipeLine内の並びだけ更新する
+    const targetPipeLine = state.boardData.pipeLineList.find(pipeLine => pipeLine.pipeLineId === pipeLineId);
+    targetPipeLine.cardList = cardList;
+  },
   setBoardData(state, { boardData }) {
     state.boardData = camelcaseKeys(boardData, { deep: true });
   },

あるPipeLineのカードを更新するupdateCardOrder(mutation)を追加し、updateCardOrder(action)内でサーバにリクエスした直後にcommit('updateCardOrder', { pipeLineId, cardList })を呼び出し、サーバの返答を待たずに自身のStore内のデータだけは更新するようにします。

これでちらつきはなくなりました。

Image from Gyazo

ブラウザ間のデータ同期

現在のボードの並び替えはブラウザ間で同期されていません。カンバンといえば、他のブラウザでの並び替えが直ちに反映されるのが特徴の一つなので、そこを実装していきます。

基本的な話

Channelsでブラウザをまたがってメッセージの送受信をするにはChannelLayerというコンポーネントを使用します。Websocketの接続ごとに生成されるConsumerインスタンスはChannelLayerを通じて相互にメッセージ受信ができるようになります。

設定の変更

ChannelLayerを有効化にするには、まずCHANNEL_LAYERSをsettingsに追加します。

application/settings/base.py
@@ -139,3 +139,13 @@ LOGOUT_REDIRECT_URL = '/'

 # ASGIの起点を指定
 ASGI_APPLICATION = 'views.routing.application'
+
+# バックエンドをRedisにする
+CHANNEL_LAYERS = {
+    'default': {
+        'BACKEND': 'channels_redis.core.RedisChannelLayer',
+        'CONFIG': {
+            "hosts": [('redis', 6379)],
+        },
+    },
+}

バックエンドはRedisを指定しています。今回使用しているテンプレートにはRedisサーバのコンテナがすでに組み込まれているので、redis:6379をURLと指定すれば使えます。

ConsumerにChannelLayerの初期化を追加

Consumerが初期化されるタイミングで、ConsumerをChannelLayerに追加します。ChannelLayerは言うなれば、チャットにおけるルームのような概念で、どのルームに所属するかを指定して初期化します。

application/views/ws/kanban_consumer.py
@@ -1,3 +1,4 @@
+from asgiref.sync import async_to_sync
 from channels.generic.websocket import JsonWebsocketConsumer

 from modules.kanban import service as kanban_sv
@@ -16,6 +17,7 @@ class KanbanConsumer(JsonWebsocketConsumer):
         self.action_map = {
             'update_card_order': self.update_card_order
         }
+        self.room_group_name = None

     def connect(self):
         # 認証チェック
@@ -28,6 +30,14 @@ class KanbanConsumer(JsonWebsocketConsumer):
         # 接続を受け入れる
         self.accept()

+        # channel layerの初期化
+        self.room_group_name = 'board_id_{}'.format(self.board_id)
+        # ボード毎のグループに参加
+        async_to_sync(self.channel_layer.group_add)(
+            self.room_group_name,
+            self.channel_name
+        )
+
         self.send_board_data()

今回は、URLに含まれているboard_idをルーム名として使ってgroup_addを行っています。なお、ChannelLayer関連の処理は非同期になっているので、同期Consumer内で使う場合はasync_to_syncデコレータを使って同期処理に変換しないといけない点に注意します。

ブロードキャスト処理の実装

今は、ConsumerとClientが1:1でメッセージをやり取りする実装になっていました。

application/views/ws/kanban_consumer.py
    def send_board_data(self):
        board_data = kanban_sv.get_board_data_by_board_id(self.board_id)
        self.send_json({
            'boardData': board_data,
            'mutation': 'setBoardData',
            'namespace': self.namespace,
        })

    def update_card_order(self, content):
        """
        ボード内のカードの並び順を更新する
        {
            'type': 'update_card_order',
            'pipeLineId': 1,
            'cardIdList': [3, 1]
        }
        :return:
        """
        pipe_line_id = content['pipeLineId']
        card_id_list = content['cardIdList']
        kanban_sv.update_card_order(pipe_line_id, card_id_list)
        self.send_board_data()

update_card_orderの完了時に呼び出されるsend_board_dataはConsumerと紐づくClientにだけ新しいボードのデータを返送する実装です。これを同じroom_group_nameに所属しているConsumerすべてにsend_board_dataを呼び出させるようにします。

ChannelLayerを通じてメッセージを送るにはself.channel_layer.group_sendを使用します。また、非同期処理なのでこちらもsync_to_asyncでラップします。

application/views/ws/kanban_consumer.py
@@ -40,7 +40,7 @@ class KanbanConsumer(JsonWebsocketConsumer):

         self.send_board_data()

-    def send_board_data(self):
+    def send_board_data(self, event=None, *args, **kwargs):
         board_data = kanban_sv.get_board_data_by_board_id(self.board_id)
         self.send_json({
             'boardData': board_data,
@@ -61,7 +61,13 @@ class KanbanConsumer(JsonWebsocketConsumer):
         pipe_line_id = content['pipeLineId']
         card_id_list = content['cardIdList']
         kanban_sv.update_card_order(pipe_line_id, card_id_list)
-        self.send_board_data()
+        # 同じグループ(自身も含む)に`send_board_data`を呼び出すように通知
+        async_to_sync(self.channel_layer.group_send)(
+            self.room_group_name,
+            {
+                'type': 'send_board_data',
+            }
+        )

     def receive_json(self, content, **kwargs):
         """

もともとupdate_card_orderの最後でsend_board_dataを呼び出していた部分をself.channel_layer.group_sendに置き換えています。この時、第一引数にはどのグループにメッセージを通知するかを指定するので、ここでは自身と同じroom_group_nameを指定します。第二引数にはメッセージ内容を指定しますが、大事なのはtypeキーです。ここに文字列で指定したメソッド名が各Consumerで呼び出されるようになります。

そのため、この部分のコードでは、自身と同じroom_group_nameに所属しているConsumerすべてにメッセージを送信し、受け取ったConsumer(送信元自身のConsumerも含む)がtypeに指定されたsend_board_dataが呼び出され、それぞれのConsumerに紐付いたClientに新しいボードデータを戻します。

2つのブラウザで開いてるときを例にざっくりとした処理の流れは以下のようになります。

  1. Client1がカード並び替えを実行
  2. Consumer1が新しい並びでカードを更新
  3. Consumer1が同じGroupに所属するConsumerにメッセージを送信
  4. Consumer1,Consumer2がメッセージを受信
  5. Consumer1がClient1,Consumer2がClient2に新しい並びでのボードデータを送信
  6. Client1,Client2が新しい並びで再レンダリング

実際にこんな感じで複数ブラウザで処理が同期しています。

Image from Gyazo

だいぶ完成に近づいてきました。

次回

4
6
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
4
6