※はじめからはこちら
前回まででカードの並び替えはできるようになりましたが、一瞬カードが戻ってしまう状況だったのでそのあたりを改善していきます。
一瞬もどる原因
前回実装した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 });
},
};
カードの並び順の更新は、以下の流れになります。
- Componentから
updateCardOrder
(action)を呼び出し - Websocket経由で情報の更新をサーバにリクエストし
- サーバ側がWebsocketで新しいカードの並び順(正確にばボード全体のデータ)を返送し
-
setBoardData
(mutation)が実行される
Component側で、D&Dが完了するのは1.ですが、新しい並び順になるのは4.まで完了した時点です。そのため、その間はD&Dが完了しても、その前のデータがレンダリングされてしまうのでチラツキが発生します。Websocketではサーバが次のメッセージを戻すまで待つといったことができないので、まっとうには避けづらいです。
対策としては、おそらく処理は完了するだろうという楽観的な視点にたって処理を実行したクライアントではサーバのメッセージを待たずにデータを更新してしまうことが有効です。
Storeの変更
まずは、Action/Mutationを変更します。
@@ -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内のデータだけは更新するようにします。
これでちらつきはなくなりました。
ブラウザ間のデータ同期
現在のボードの並び替えはブラウザ間で同期されていません。カンバンといえば、他のブラウザでの並び替えが直ちに反映されるのが特徴の一つなので、そこを実装していきます。
基本的な話
Channelsでブラウザをまたがってメッセージの送受信をするにはChannelLayer
というコンポーネントを使用します。Websocketの接続ごとに生成されるConsumerインスタンスはChannelLayer
を通じて相互にメッセージ受信ができるようになります。
設定の変更
ChannelLayerを有効化にするには、まずCHANNEL_LAYERS
をsettingsに追加します。
@@ -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は言うなれば、チャットにおけるルームのような概念で、どのルームに所属するかを指定して初期化します。
@@ -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でメッセージをやり取りする実装になっていました。
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
でラップします。
@@ -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つのブラウザで開いてるときを例にざっくりとした処理の流れは以下のようになります。
- Client1がカード並び替えを実行
- Consumer1が新しい並びでカードを更新
- Consumer1が同じGroupに所属するConsumerにメッセージを送信
- Consumer1,Consumer2がメッセージを受信
- Consumer1がClient1,Consumer2がClient2に新しい並びでのボードデータを送信
- Client1,Client2が新しい並びで再レンダリング
実際にこんな感じで複数ブラウザで処理が同期しています。
だいぶ完成に近づいてきました。