Slide 1

Slide 1 text

%KBOHPͩͬͯ Χϯόϯͭ͘ΕΔ΋Μ %KBOHP$IBOOFMTͰ࡞Ζ͏ΧϯόϯΞϓϦέʔγϣϯ 1Z$PO+1 !EFO[PXJMMPSEFO[PX

Slide 2

Slide 2 text

2 タイトルを書いてたときは酔ってた。 今は後悔している

Slide 3

Slide 3 text

3 ➡ でんぞう (@denzowill, denzow) ➡ scouty,inc シニアエンジニア ➡ Scrapy, Djangoあたり ➡ DBスペシャリスト(PostgreSQLが好き、●racleは得意だけど苦⼿) ➡ (元?)StartPythonClubスタッフ お前誰よ?

Slide 4

Slide 4 text

4 お前誰よ? https://speakerdeck.com/denzow/imasarazhen-rifan-ru-django-migration https://qiita.com/denzow/items/77df4b45cfbbf2f0df92

Slide 5

Slide 5 text

5 お前誰よ? https://speakerdeck.com/denzow/imasarazhen-rifan-ru-django-migration https://qiita.com/denzow/items/77df4b45cfbbf2f0df92 全90P は50分には ⻑すぎた

Slide 6

Slide 6 text

6 お前誰よ?

Slide 7

Slide 7 text

7 お前誰よ? 170Pを45分で お送りします

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

のミッション ⾃分のまわりには、⾃分でも気づいていないたくさんの可能性や偶然性が存在するはずなのに、
 ⼈はいつもそれに巡り会えるとは限りません。
 そしてその結果、仕事や⼈材におけるミスマッチに悩む⼈も少なくはないでしょう。 scoutyは、インターネット上にあふれるデータと最先端の⼈⼯知能技術を使って情報と機会を適 切にお届けすることで、偶然を必然に変え、世の中のミスマッチをなくしていくことを⽬指しま す。 そして、それは結果として、個⼈の市場価値や⽣活の質を⾼め、企業の競争⼒を⾼めること につながると考えています。 「世の中のミスマッチを無くす」

Slide 10

Slide 10 text

⽉1で麹町あたりで 100⼈くらいの勉強会やってました 11⽉からは新橋 https://startpython.connpass.com/

Slide 11

Slide 11 text

Pythonと

Slide 12

Slide 12 text

インフラ構成図 Amazon
 DynamoD Amazon ECS Amazon ECS Amazon ECS Amazon ECS Amazon
 SQS Elastic Load AWS Lambda Amazon CloudWatch Amazon
 RDS Aurora
 (MySQL 5.7) Amazon 
 ElastiCache sns-activity
 watcher worker ϝΠϯαʔϏε Ϋϩʔϧͨ͠
 ੜσʔλͷdiff ੔ܗ͞Εͨσʔ λ event 
 (time- crawler

Slide 13

Slide 13 text

インフラ構成図 Amazon
 DynamoD Amazon ECS Amazon ECS Amazon ECS Amazon ECS Amazon
 SQS Elastic Load AWS Lambda Amazon CloudWatch Amazon
 RDS Aurora
 (MySQL 5.7) Amazon 
 ElastiCache sns-activity
 watcher worker ϝΠϯαʔϏε Ϋϩʔϧͨ͠
 ੜσʔλͷdiff ੔ܗ͞Εͨσʔ λ event 
 (time- crawler

Slide 14

Slide 14 text

インフラ構成図 Amazon
 DynamoD Amazon ECS Amazon ECS Amazon ECS Amazon ECS Amazon
 SQS Elastic Load AWS Lambda Amazon CloudWatch Amazon
 RDS Aurora
 (MySQL 5.7) Amazon 
 ElastiCache sns-activity
 watcher worker ϝΠϯαʔϏε Ϋϩʔϧͨ͠
 ੜσʔλͷdiff ੔ܗ͞Εͨσʔ λ event 
 (time- crawler

Slide 15

Slide 15 text

15 お詫び

Slide 16

Slide 16 text

16

Slide 17

Slide 17 text

17 Beginnerの定義読み違えた

Slide 18

Slide 18 text

18 https://qiita.com/denzow/items/046f3c8b9bd8d3378eb4 Beginner向けにチュートリアル書いてくので許して

Slide 19

Slide 19 text

19 本題

Slide 20

Slide 20 text

20

Slide 21

Slide 21 text

21

Slide 22

Slide 22 text

22 カンバンっていいよね

Slide 23

Slide 23 text

23 Djangoでカンバン つくりたい

Slide 24

Slide 24 text

24 Djangoでカンバン つくろう!

Slide 25

Slide 25 text

25 カンバンに必要な機能 ドラッグアンドドロップでの 直感的な操作 リアルタイムなデータの 反映 複数⼈での コラボレーション

Slide 26

Slide 26 text

26 カンバンに必要な機能 ドラッグアンドドロップでの 直感的な操作 リアルタイムなデータの 反映 複数⼈での コラボレーション クライアントサイドの JS頑張る WebSocketで リアルタイム処理をする

Slide 27

Slide 27 text

27 カンバンに必要な機能 ドラッグアンドドロップでの 直感的な操作 リアルタイムなデータの 反映 複数⼈での コラボレーション クライアントサイドの JS頑張る WebSocketで リアルタイム処理をする

Slide 28

Slide 28 text

28 カンバンに必要な機能 ドラッグアンドドロップでの 直感的な操作 リアルタイムなデータの 反映 複数⼈での コラボレーション クライアントサイドの JS頑張る WebSocketで リアルタイム処理をする

Slide 29

Slide 29 text

DjangoでWebSocketの 何が⾟いのか

Slide 30

Slide 30 text

30

Slide 31

Slide 31 text

31

Slide 32

Slide 32 text

32 Djangoは普通WSGIで動かす

Slide 33

Slide 33 text

33 WSGI? アプリケーションとフレームワーク間の規格 PEP333(Python2)/ PEP3333(Python3) Web Server A Web Server B Web Server C Framework A Framework B Framework C

Slide 34

Slide 34 text

34 WSGI? アプリケーションとフレームワーク間の規格 PEP333(Python2)/ PEP3333(Python3) Web Server A Web Server B Web Server C Framework A Framework B Framework C

Slide 35

Slide 35 text

35 WSGI? アプリケーションとフレームワーク間の規格 PEP333(Python2)/ PEP3333(Python3) Web Server A Web Server B Web Server C Framework A Framework B Framework C WSGI

Slide 36

Slide 36 text

36 WSGI? アプリケーションとフレームワーク間の規格 PEP333(Python2)/ PEP3333(Python3) uWSGI Gunicorn Werkzeug Django Flask Bottle WSGI

Slide 37

Slide 37 text

37 WSGI? PEP 333の提案⽇は2003-12-07 https://www.python.org/dev/peps/pep-0333/

Slide 38

Slide 38 text

38 WSGI? websocketが⽣まれたのは2011頃 https://ja.wikipedia.org/wiki/WebSocket

Slide 39

Slide 39 text

39 WSGI? NO WebSocket

Slide 40

Slide 40 text

40 WSGIはWebSocketより前に⽣まれた def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'Hello World\n'] start_responseでステータスやヘッダを送信 サーバがwsgiのエンドポイントのcallableを呼び出す Callableはenvironとstart_responseを受け取る レスポンス本体をiterableな戻り値として返送

Slide 41

Slide 41 text

41 WSGIはWebSocketより前に⽣まれた def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'Hello World\n'] start_responseでステータスやヘッダを送信 1リクエスト毎にCallbleが呼ばれ 終了する サーバがwsgiのエンドポイントのcallableを呼び出す Callableはenvironとstart_responseを受け取る レスポンス本体をiterableな戻り値として返送

Slide 42

Slide 42 text

42 Websocketのライフサイクル 確⽴したセッションでメッセージの送受信をする 初回接続時にセッションを確⽴ セッションは切断処理がされるまで確⽴されたまま サーバからもプッシュ形式でメッセージが届く

Slide 43

Slide 43 text

43 Websocketのライフサイクル 確⽴したセッションでメッセージの送受信をする 初回接続時にセッションを確⽴ セッションは切断処理がされるまで確⽴されたまま サーバからもプッシュ形式でメッセージが届く Websocket の ライフサイクルは⻑い

Slide 44

Slide 44 text

44 WSGIでWebsocket つらい

Slide 45

Slide 45 text

45 ASGI誕⽣ Asynchronous Server Gateway Interface

Slide 46

Slide 46 text

46 https://asgi.readthedocs.io/en/latest/introduction.html

Slide 47

Slide 47 text

47 ASGI? WSGIで対応が難しいWebSocket等のための規格 WSGIのスーパーセットになるようにする

Slide 48

Slide 48 text

48 ASGI? WSGIで対応が難しいWebSocket等のための規格 WSGIのスーパーセットになるようにする Web Server A Web Server B Web Server C Framework A Framework B Framework C WSGI

Slide 49

Slide 49 text

49 ASGI? WSGIで対応が難しいWebSocket等のための規格 WSGIのスーパーセットになるようにする Web Server A Web Server B Web Server C ASGI ASGI Framework A ASGI Framework B ASGI Framework C

Slide 50

Slide 50 text

50 ひろがる Python

Slide 51

Slide 51 text

実際にカンバン つくってみた

Slide 52

Slide 52 text

52 DjangoDeKanban https://github.com/denzow/DjangoDeKanban

Slide 53

Slide 53 text

53 DjangoDeKanban

Slide 54

Slide 54 text

54 DjangoDeKanban ボード作成 パイプライン(リスト)追加 カード追加 パイプライン(リスト)の並び替え カード並び替え ログイン管理 キーワードでのカード絞り込み

Slide 55

Slide 55 text

55 DjangoDeKanban Django Channels 2.1 Vue/Vuex VueDraggable VueNativeWebSocket Django 2.1

Slide 56

Slide 56 text

56 DjangoDeKanban Django Channels 2.1 Vue/Vuex VueDraggable VueNativeWebSocket Django 2.1

Slide 57

Slide 57 text

Django Channels 2

Slide 58

Slide 58 text

58 Django Channels 1.xの話はしません 2.xと1.xはPython 2とPython 3くらい違います

Slide 59

Slide 59 text

59 Django Channels https://channels.readthedocs.io/en/latest/ Django Channelsは DjangoをASGI対応にするライブラリ

Slide 60

Slide 60 text

60 Django Channels https://github.com/django/channels Djangoグループが開発している

Slide 61

Slide 61 text

61 Django Channelsでの流れ Client(Browser) ProtocolTypeRouter URLRouter Consumer

Slide 62

Slide 62 text

62 Django Channelsでの流れ(ProtocolTypeRouter) application/settings/base.py # ASGIの起点を指定 ASGI_APPLICATION = 'views.routing.application' application/views/routing.py from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter # Childのルーティングルールに分割 from .ws.routing import urlpatterns application = ProtocolTypeRouter({ # (http->django views is added by default) 'websocket': AuthMiddlewareStack( URLRouter( urlpatterns ) ), }) プロトコル毎の 処理振り分け

Slide 63

Slide 63 text

63 Django Channelsでの流れ(ProtocolTypeRouter) application/settings/base.py # ASGIの起点を指定 ASGI_APPLICATION = 'views.routing.application' application/views/routing.py from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter # Childのルーティングルールに分割 from .ws.routing import urlpatterns application = ProtocolTypeRouter({ # (http->django views is added by default) 'websocket': AuthMiddlewareStack( URLRouter( urlpatterns ) ), }) Websocketの場合 httpは書かなくても 勝⼿にurls.pyをもとに したものが追加される

Slide 64

Slide 64 text

64 Django Channelsでの流れ(ProtocolTypeRouter) application/settings/base.py # ASGIの起点を指定 ASGI_APPLICATION = 'views.routing.application' application/views/routing.py from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter # Childのルーティングルールに分割 from .ws.routing import urlpatterns application = ProtocolTypeRouter({ # (http->django views is added by default) 'websocket': AuthMiddlewareStack( URLRouter( urlpatterns ) ), }) Djangoの認証と 同じものを使えるように する

Slide 65

Slide 65 text

65 Django Channelsでの流れ(URLRouter) application/settings/base.py # ASGIの起点を指定 ASGI_APPLICATION = 'views.routing.application' application/views/routing.py from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter # Childのルーティングルールに分割 from .ws.routing import urlpatterns application = ProtocolTypeRouter({ # (http->django views is added by default) 'websocket': AuthMiddlewareStack( URLRouter( urlpatterns ) ), }) どのようなURLにアクセス してきたかでの振り分け (別に直接書いてもいい)

Slide 66

Slide 66 text

66 Django Channelsでの流れ(URLRouter) application/views/ws/routing.py from django.urls import path from .consumers import kanban_consumer urlpatterns = [ path('ws/boards/', kanban_consumer.KanbanConsumer) ] 基本的にはDjangoのurls.pyと同じ

Slide 67

Slide 67 text

67 Django Channelsでの流れ(URLRouter) application/views/ws/routing.py from django.urls import path from .consumers import kanban_consumer urlpatterns = [ path('ws/boards/', kanban_consumer.KanbanConsumer) ] ws/boards/1 のような URLにマッチさせる 基本的にはDjangoのurls.pyと同じ

Slide 68

Slide 68 text

68 Django Channelsでの流れ(URLRouter) application/views/ws/routing.py from django.urls import path from .consumers import kanban_consumer urlpatterns = [ path('ws/boards/', kanban_consumer.KanbanConsumer) ] 該当したときの処理 基本的にはDjangoのurls.pyと同じ

Slide 69

Slide 69 text

69 Django Channelsでの流れ(Consumer) application/views/ws/consumers/kanban_consumer.py : : class KanbanConsumer(BaseJsonConsumer): : async def connect(self): if not self.scope['user'].is_authenticated: await self.close() return self.user = self.scope['user'] self.board_id = self.scope['url_route']['kwargs']['board_id'] self.room_group_name = self.user.username await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() : Viewsみたいなもん

Slide 70

Slide 70 text

70 Django Channelsでの流れ(Consumer) application/views/ws/consumers/kanban_consumer.py : : class KanbanConsumer(BaseJsonConsumer): : async def connect(self): if not self.scope['user'].is_authenticated: await self.close() return self.user = self.scope['user'] self.board_id = self.scope['url_route']['kwargs']['board_id'] self.room_group_name = self.user.username await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() : AuthMiddlewareStackを 使うと、scope['user']に いれてくれる Viewsみたいなもん

Slide 71

Slide 71 text

71 Django Channelsでの流れ(Consumer) application/views/ws/consumers/kanban_consumer.py : : class KanbanConsumer(BaseJsonConsumer): : async def connect(self): if not self.scope['user'].is_authenticated: await self.close() return self.user = self.scope['user'] self.board_id = self.scope['url_route']['kwargs']['board_id'] self.room_group_name = self.user.username await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() : URLで と 定義してた部分にマッチした情報が 取り出せる Viewsみたいなもん

Slide 72

Slide 72 text

72 application/views/ws/routing.py from django.urls import path from .consumers import kanban_consumer urlpatterns = [ path('ws/boards/', kanban_consumer.KanbanConsumer) ] これ Django Channelsでの流れ(Consumer)

Slide 73

Slide 73 text

73 Django Channelsでの流れ(Consumer) application/views/ws/consumers/kanban_consumer.py : : class KanbanConsumer(BaseJsonConsumer): : async def connect(self): if not self.scope['user'].is_authenticated: await self.close() return self.user = self.scope['user'] self.board_id = self.scope['url_route']['kwargs']['board_id'] self.room_group_name = self.user.username await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() : URLで と 定義してた部分にマッチした情報が 取り出せる Viewsみたいなもん

Slide 74

Slide 74 text

74 Django Channelsでの流れ(Consumer) application/views/ws/consumers/kanban_consumer.py : : class KanbanConsumer(BaseJsonConsumer): : async def connect(self): if not self.scope['user'].is_authenticated: await self.close() return self.user = self.scope['user'] self.board_id = self.scope['url_route']['kwargs']['board_id'] self.room_group_name = self.user.username await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() : ChannelLayerのgroup_addを 実⾏し、他のConsumerとの 通信ができるようにする Viewsみたいなもん

Slide 75

Slide 75 text

75 Django Channelsでの流れ(Consumer) application/views/ws/consumers/kanban_consumer.py : : class KanbanConsumer(BaseJsonConsumer): : async def connect(self): if not self.scope['user'].is_authenticated: await self.close() return self.user = self.scope['user'] self.board_id = self.scope['url_route']['kwargs']['board_id'] self.room_group_name = self.user.username await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() : 接続を受け⼊れる 接続を拒否する Viewsみたいなもん

Slide 76

Slide 76 text

76 もうちょっとConsumer class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): # accept or close if YourCondition: await self.accept() else: await self.close() async def receive_json(self, content, **kwargs): # receive message print(content) # echo back await self.send_json(content)

Slide 77

Slide 77 text

77 もうちょっとConsumer class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): # accept or close if YourCondition: await self.accept() else: await self.close() async def receive_json(self, content, **kwargs): # receive message print(content) # echo back await self.send_json(content) (Async)?JsonWebsocketConsumerを 継承して実装する

Slide 78

Slide 78 text

78 もうちょっとConsumer class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): # accept or close if YourCondition: await self.accept() else: await self.close() async def receive_json(self, content, **kwargs): # receive message print(content) # echo back await self.send_json(content) 初回接続時に 呼ばれる Accept or close を 呼び出す

Slide 79

Slide 79 text

79 もうちょっとConsumer class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): # accept or close if YourCondition: await self.accept() else: await self.close() async def receive_json(self, content, **kwargs): # receive message print(content) # echo back await self.send_json(content) クライアントからの メッセージ受信時に呼ばれる

Slide 80

Slide 80 text

80 もうちょっとConsumer class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): # accept or close if YourCondition: await self.accept() else: await self.close() async def receive_json(self, content, **kwargs): # receive message print(content) # echo back await self.send_json(content) クライアントへ メッセージを返送する

Slide 81

Slide 81 text

81 Client1 Consumer1 Client2 Client3 Websocket Consumer2 Consumer3 ClientとConsumer間はWebsocketで 送受信ができるようになった

Slide 82

Slide 82 text

82 Client1 Consumer1 Client2 Client3 Consumer2 Consumer3 Consumer1の変更をClient2,3に伝えるには? card_list = [A] card_list = [A] card_list = [A] card_list = [A] card_list = [A] card_list = [A]

Slide 83

Slide 83 text

83 Client1 Consumer1 Client2 Client3 Add card B Consumer2 Consumer3 Consumer1の変更をClient2,3に伝えるには? card_list = [A, B] card_list = [A, B] card_list = [A] card_list = [A] card_list = [A] card_list = [A]

Slide 84

Slide 84 text

84 Client1 Consumer1 Client2 Client3 Add card B Consumer2 Consumer3 Consumer1の変更をClient2,3に伝えるには? card_list = [A, B] card_list = [A, B] card_list = [A] card_list = [A] card_list = [A] card_list = [A] Channel Layer

Slide 85

Slide 85 text

85 Client1 Consumer1 Client2 Client3 Add card B Consumer2 Consumer3 Consumer1の変更をClient2,3に伝えるには? card_list = [A, B] card_list = [A, B] card_list = [A] card_list = [A] card_list = [A] card_list = [A] Channel Layer Plz get Get Get

Slide 86

Slide 86 text

86 Client1 Consumer1 Client2 Client3 Add card B Consumer2 Consumer3 Consumer1の変更をClient2,3に伝えるには? card_list = [A, B] card_list = [A, B] card_list = [A] card_list = [A, B] card_list = [A] card_list = [A, B] Channel Layer Plz get Get Get

Slide 87

Slide 87 text

87 Client1 Consumer1 Client2 Client3 Add card B Consumer2 Consumer3 Consumer1の変更をClient2,3に伝えるには? card_list = [A, B] card_list = [A, B] card_list = [A, B] card_list = [A, B] card_list = [A, B] card_list = [A, B] Channel Layer Plz get Get Get

Slide 88

Slide 88 text

88 もうちょっとChannelLayer class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): : self.group_name = 'group_1' await self.channel_layer.group_add( self.group_name, self.channel_name # auto injection ) async def receive_json(self, content, **kwargs): : # broadcast other consumers await self.channel_layer.group_send( self.group_name, { 'type': 're_send', } ) async def re_send(self, *args, **kwargs): await self.send_json('new_content')

Slide 89

Slide 89 text

89 もうちょっとChannelLayer class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): : self.group_name = 'group_1' await self.channel_layer.group_add( self.group_name, self.channel_name # auto injection ) async def receive_json(self, content, **kwargs): : # broadcast other consumers await self.channel_layer.group_send( self.group_name, { 'type': 're_send', } ) async def re_send(self, *args, **kwargs): await self.send_json('new_content') connect時に 処理内容を共有する グループへ登録

Slide 90

Slide 90 text

90 もうちょっとChannelLayer class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): : self.group_name = 'group_1' await self.channel_layer.group_add( self.group_name, self.channel_name # auto injection ) async def receive_json(self, content, **kwargs): : # broadcast other consumers await self.channel_layer.group_send( self.group_name, { 'type': 're_send', } ) async def re_send(self, *args, **kwargs): await self.send_json('new_content') group_sendで ChannelLayerを 通じて他のConsumerに リクエストする

Slide 91

Slide 91 text

91 もうちょっとChannelLayer class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): : self.group_name = 'group_1' await self.channel_layer.group_add( self.group_name, self.channel_name # auto injection ) async def receive_json(self, content, **kwargs): : # broadcast other consumers await self.channel_layer.group_send( self.group_name, { 'type': 're_send', } ) async def re_send(self, *args, **kwargs): await self.send_json('new_content') typeで指定したメソッドが 各Consumerで実⾏される

Slide 92

Slide 92 text

92 実際のコードでは application/views/ws/consumers/kanban_consumer.py def __init__(self, *args, **kwargs): : self.action_map = { 'update_card_order': self.update_card_order, 'update_pipe_line_order': self.update_pipe_line_order, 'add_pipe_line': self.add_pipe_line, 'add_card': self.add_card, 'rename_pipe_line': self.rename_pipe_line, 'delete_pipe_line': self.delete_pipe_line, 'delete_board': self.delete_board, 'rename_board': self.rename_board, 'broadcast_board_data': self.broadcast_board_data, 'broadcast_board_data_without_requester': self.broadcast_board_data_without_requester, }

Slide 93

Slide 93 text

93 実際のコードでは application/views/ws/consumers/base_consumer.py async def receive_json(self, content, **kwargs): """ Typeに応じた処理を呼び出して実⾏する :param dict content: :param kwargs: :return: """ action = self.action_map.get(content['type']) if not action: raise ConsumerException('{} is not a valid action_type'.format(content['type'])) await action(content) content.typeに応じたメソッドを 呼び出すようにreceive_jsonを オーバライド

Slide 94

Slide 94 text

カンバンとVue

Slide 95

Slide 95 text

95 Vueがカンバンに向いてるということではなく 単に私がVueでつくったので紹介だけ

Slide 96

Slide 96 text

96 カンバンに必要な機能 ドラッグアンドドロップでの 直感的な操作 リアルタイムなデータの 反映 クライアントサイドの JS頑張る

Slide 97

Slide 97 text

97 ドラッグアンドドロップ

Slide 98

Slide 98 text

98 Vue Draggable https://github.com/SortableJS/Vue.Draggable

Slide 99

Slide 99 text

99 Vue Draggable Sortable.jsのラッパーで使いやすい リスト内はもちろん、リスト間のD&Dも容易 D&D完了時に、並び順のデータがちゃんと取れる

Slide 100

Slide 100 text

100 Vue Draggable リスト間移動 リスト内移動

Slide 101

Slide 101 text

101 VueNativeWebsocket https://github.com/nathantsoi/vue-native-websocket

Slide 102

Slide 102 text

102 VueNativeWebsocket

Slide 103

Slide 103 text

103 VueNativeWebsocket ☺

Slide 104

Slide 104 text

104 VueNativeWebsocket Vue.use(VueNativeSock, 'ws://localhost:9090', { store, }) Vue側から簡単にWebsocketが使える Store(Vuex)とも連携が簡単 Serverからmutation/actionが呼び出せる

Slide 105

Slide 105 text

105 VueNativeWebsocket socket.sendObj({ type: 'add_card', pipeLineId, cardTitle, }); sendObjでJSONを簡単に投げられる

Slide 106

Slide 106 text

106 VueNativeWebsocket シンプルな⽤途であれば使いやすくて便利 ⾃動再接続や⼿動接続のメソッドもある

Slide 107

Slide 107 text

107 VueNativeWebsocket 接続先の変更とかはちょっとだるい // 切断 Vue.prototype.$disconnect(); // InstallされているVueNativeSockを⼀旦削除する const index = Vue._installedPlugins.indexOf(VueNativeSock); if (index > -1) { Vue._installedPlugins.splice(index, 1); } // 新しいURLで再インストールする Vue.use(VueNativeSock, `/new_ws_endpoint/`, { connectManually: true, reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 3000, format: 'json', store, }); // 新しいURLへ接続する Vue.prototype.$connect();

Slide 108

Slide 108 text

データの流れ

Slide 109

Slide 109 text

109

Slide 110

Slide 110 text

110 データの流れ(VueComponent) : computed: { wrappedCardList: { get() { return this.pipeLine.cardList; }, set(value) { console.log(value); this.updateCardOrder({ pipeLineId: this.pipeLine.pipeLineId, cardList: value, }); }, }, application/vuejs/src/pages/Board/components/BoardArea/PipeLine.vue

Slide 111

Slide 111 text

111 データの流れ(VueComponent) : computed: { wrappedCardList: { get() { return this.pipeLine.cardList; }, set(value) { console.log(value); this.updateCardOrder({ pipeLineId: this.pipeLine.pipeLineId, cardList: value, }); }, }, Draggableで 囲んだ要素(Card)が D&D可能に D&Dの更新内容は V-modelに同期される 作り application/vuejs/src/pages/Board/components/BoardArea/PipeLine.vue

Slide 112

Slide 112 text

112 データの流れ(VueComponent) : computed: { wrappedCardList: { get() { return this.pipeLine.cardList; }, set(value) { console.log(value); this.updateCardOrder({ pipeLineId: this.pipeLine.pipeLineId, cardList: value, }); }, }, D&D完了時にsetが 呼ばれる [ {cardId: 1, title: ....}, {cardId: 2, title: ....}, {cardId: 3, title: ....}, ] application/vuejs/src/pages/Board/components/BoardArea/PipeLine.vue

Slide 113

Slide 113 text

113 データの流れ(VueComponent) : computed: { wrappedCardList: { get() { return this.pipeLine.cardList; }, set(value) { console.log(value); this.updateCardOrder({ pipeLineId: this.pipeLine.pipeLineId, cardList: value, }); }, }, Vuexに処理を 依頼する application/vuejs/src/pages/Board/components/BoardArea/PipeLine.vue

Slide 114

Slide 114 text

114 データの流れ(Vuex) // ACTION updateCardOrder({ commit, getters }, { pipeLineId, cardList }) { const socket = getters.getSocket; socket.sendObj({ type: 'update_card_order', pipeLineId, cardIdList: cardList.map(x => x.cardId), }); commit('updateCardOrder', { pipeLineId, cardList }); }, // MUTATION updateCardOrder(state, { pipeLineId, cardList }) { const targetPipeLine = state.boardData.pipeLineList .find(pipeLine => pipeLine.pipeLineId === pipeLineId); targetPipeLine.cardList = cardList; }, application/vuejs/src/store/pages/board.js

Slide 115

Slide 115 text

115 データの流れ(Vuex) // ACTION updateCardOrder({ commit, getters }, { pipeLineId, cardList }) { const socket = getters.getSocket; socket.sendObj({ type: 'update_card_order', pipeLineId, cardIdList: cardList.map(x => x.cardId), }); commit('updateCardOrder', { pipeLineId, cardList }); }, // MUTATION updateCardOrder(state, { pipeLineId, cardList }) { const targetPipeLine = state.boardData.pipeLineList .find(pipeLine => pipeLine.pipeLineId === pipeLineId); targetPipeLine.cardList = cardList; }, application/vuejs/src/store/pages/board.js VueComponentから ここが呼ばれる。

Slide 116

Slide 116 text

116 データの流れ(Vuex) // ACTION updateCardOrder({ commit, getters }, { pipeLineId, cardList }) { const socket = getters.getSocket; socket.sendObj({ type: 'update_card_order', pipeLineId, cardIdList: cardList.map(x => x.cardId), }); commit('updateCardOrder', { pipeLineId, cardList }); }, // MUTATION updateCardOrder(state, { pipeLineId, cardList }) { const targetPipeLine = state.boardData.pipeLineList .find(pipeLine => pipeLine.pipeLineId === pipeLineId); targetPipeLine.cardList = cardList; }, application/vuejs/src/store/pages/board.js VueNativeWebsocketを 使って、Django側に更新を 依頼(後述)

Slide 117

Slide 117 text

117 データの流れ(Vuex) // ACTION updateCardOrder({ commit, getters }, { pipeLineId, cardList }) { const socket = getters.getSocket; socket.sendObj({ type: 'update_card_order', pipeLineId, cardIdList: cardList.map(x => x.cardId), }); commit('updateCardOrder', { pipeLineId, cardList }); }, // MUTATION updateCardOrder(state, { pipeLineId, cardList }) { const targetPipeLine = state.boardData.pipeLineList .find(pipeLine => pipeLine.pipeLineId === pipeLineId); targetPipeLine.cardList = cardList; }, application/vuejs/src/store/pages/board.js 投げると同時に投げた クライアント側では更新が ⾏われた体で表⽰を更新

Slide 118

Slide 118 text

118 データの流れ(Vuex) // ACTION updateCardOrder({ commit, getters }, { pipeLineId, cardList }) { const socket = getters.getSocket; socket.sendObj({ type: 'update_card_order', pipeLineId, cardIdList: cardList.map(x => x.cardId), }); commit('updateCardOrder', { pipeLineId, cardList }); }, // MUTATION updateCardOrder(state, { pipeLineId, cardList }) { const targetPipeLine = state.boardData.pipeLineList .find(pipeLine => pipeLine.pipeLineId === pipeLineId); targetPipeLine.cardList = cardList; }, application/vuejs/src/store/pages/board.js

Slide 119

Slide 119 text

119 データの流れ(Django Channels) application/views/ws/consumers/kanban_consumer.py class KanbanConsumer(BaseJsonConsumer): def __init__(self, *args, **kwargs): : self.action_map = { 'update_card_order': self.update_card_order, : } :

Slide 120

Slide 120 text

120 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): def __init__(self, *args, **kwargs): : self.action_map = { 'update_card_order': self.update_card_order, : } : application/views/ws/consumers/kanban_consumer.py type: 'update_card_order'は self.update_card_orderを 呼び出しするようにマップ

Slide 121

Slide 121 text

121 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): async def update_card_order(self, content): """ ボード内のカードの並び順を更新する """ pipe_line_id = content['pipeLineId'] card_id_list = content['cardIdList'] await database_sync_to_async(kanban_sv.update_card_order)( pipe_line_id, card_id_list ) await self.broadcast_board_data_without_requester() application/views/ws/consumers/kanban_consumer.py

Slide 122

Slide 122 text

122 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): async def update_card_order(self, content): """ ボード内のカードの並び順を更新する """ pipe_line_id = content['pipeLineId'] card_id_list = content['cardIdList'] await database_sync_to_async(kanban_sv.update_card_order)( pipe_line_id, card_id_list ) await self.broadcast_board_data_without_requester() application/views/ws/consumers/kanban_consumer.py DjangoORM経由でカードの 並び順を更新

Slide 123

Slide 123 text

123 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): async def update_card_order(self, content): """ ボード内のカードの並び順を更新する """ pipe_line_id = content['pipeLineId'] card_id_list = content['cardIdList'] await database_sync_to_async(kanban_sv.update_card_order)( pipe_line_id, card_id_list ) await self.broadcast_board_data_without_requester() application/views/ws/consumers/kanban_consumer.py 更新処理をした Consumer以外に ブロードキャスト

Slide 124

Slide 124 text

124 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): async def update_card_order(self, content): """ ボード内のカードの並び順を更新する """ pipe_line_id = content['pipeLineId'] card_id_list = content['cardIdList'] await database_sync_to_async(kanban_sv.update_card_order)( pipe_line_id, card_id_list ) await self.broadcast_board_data_without_requester() application/views/ws/consumers/kanban_consumer.py

Slide 125

Slide 125 text

125 // ACTION updateCardOrder({ commit, getters }, { pipeLineId, cardList }) { const socket = getters.getSocket; socket.sendObj({ type: 'update_card_order', pipeLineId, cardIdList: cardList.map(x => x.cardId), }); commit('updateCardOrder', { pipeLineId, cardList }); }, // MUTATION updateCardOrder(state, { pipeLineId, cardList }) { const targetPipeLine = state.boardData.pipeLineList .find(pipeLine => pipeLine.pipeLineId === pipeLineId); targetPipeLine.cardList = cardList; }, application/vuejs/src/store/pages/board.js 処理を依頼したクライアントは すでに更新できた体で 再描画済み

Slide 126

Slide 126 text

126 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): : async def broadcast_board_data_without_requester(self, *args): await self.group_send( self.room_group_name, { 'type': 'send_board_data', 'requester_id': self.consumer_id, } ) application/views/ws/consumers/kanban_consumer.py

Slide 127

Slide 127 text

127 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): : async def broadcast_board_data_without_requester(self, *args): await self.group_send( self.room_group_name, { 'type': 'send_board_data', 'requester_id': self.consumer_id, } ) application/views/ws/consumers/kanban_consumer.py channel_layerのgroup_sendで 他のConsumerにsend_board_dataの 実⾏を依頼

Slide 128

Slide 128 text

128 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): : async def send_board_data(self, event): """ ボードのデータを送る """ # ⾃⾝が発⽕したブロードキャストなら無視する if event.get('requester_id') == self.consumer_id: return board_data = await database_sync_to_async( kanban_sv.get_board_data_board_id )(self.board_id) await self.send_data({ 'boardData': board_data, }, mutation='setBoardData') application/views/ws/consumers/kanban_consumer.py

Slide 129

Slide 129 text

129 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): : async def send_board_data(self, event): """ ボードのデータを送る """ # ⾃⾝が発⽕したブロードキャストなら無視する if event.get('requester_id') == self.consumer_id: return board_data = await database_sync_to_async( kanban_sv.get_board_data_board_id )(self.board_id) await self.send_data({ 'boardData': board_data, }, mutation='setBoardData') application/views/ws/consumers/kanban_consumer.py Django ORM経由でボードの 構成データを再取得

Slide 130

Slide 130 text

130 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): : async def send_board_data(self, event): """ ボードのデータを送る """ # ⾃⾝が発⽕したブロードキャストなら無視する if event.get('requester_id') == self.consumer_id: return board_data = await database_sync_to_async( kanban_sv.get_board_data_board_id )(self.board_id) await self.send_data({ 'boardData': board_data, }, mutation='setBoardData') application/views/ws/consumers/kanban_consumer.py 新しいボードデータを 対応するクライアントに送信

Slide 131

Slide 131 text

131 データの流れ(Django Channels) class KanbanConsumer(BaseJsonConsumer): : async def send_board_data(self, event): """ ボードのデータを送る """ # ⾃⾝が発⽕したブロードキャストなら無視する if event.get('requester_id') == self.consumer_id: return board_data = await database_sync_to_async( kanban_sv.get_board_data_board_id )(self.board_id) await self.send_data({ 'boardData': board_data, }, mutation='setBoardData') application/views/ws/consumers/kanban_consumer.py クライアント側の mutation:setBoardDataに 引き渡す

Slide 132

Slide 132 text

132 データの流れ(Vuex) const state = { boardData: { pipeLineList: [], }, focusedCard: {}, searchWord: '', }; // MUTATION setBoardData(state, { boardData }) { state.boardData = camelcaseKeys(boardData, { deep: true }); }, application/vuejs/src/store/pages/board.js

Slide 133

Slide 133 text

133 データの流れ(Vuex) const state = { boardData: { pipeLineList: [], }, focusedCard: {}, searchWord: '', }; // MUTATION setBoardData(state, { boardData }) { state.boardData = camelcaseKeys(boardData, { deep: true }); }, application/vuejs/src/store/pages/board.js 返送されたデータで 更新し画⾯に反映

Slide 134

Slide 134 text

134 データの流れ(まとめ) D&D(Client1) Vueで変更検知 Vuexに伝播 Consumerに依頼 ORMで更新 BroadCast send_data Vuexに反映 再描画(Client1) Vuexに反映 再描画(Client2) send_data Vuexに反映 再描画(Client3)

Slide 135

Slide 135 text

つらい話

Slide 136

Slide 136 text

136 たった5⼈でサービスを落とす⽅法

Slide 137

Slide 137 text

137 カンバン周りの開発を終えてRelease直前に 動作確認をしてたら5⼈くらいでサーバごとハング

Slide 138

Slide 138 text

138 Consumer復習 class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): # accept or close if YourCondition: self.accept() else: self.close() async def receive_json(self, content, **kwargs): # receive message print(content) # echo back await self.send_json(content)

Slide 139

Slide 139 text

139 Consumer復習 class MyConsumer(AsyncJsonWebsocketConsumer or JsonWebsocketConsumer): async def connect(self): # accept or close if YourCondition: self.accept() else: self.close() async def receive_json(self, content, **kwargs): # receive message print(content) # echo back await self.send_json(content) or ?

Slide 140

Slide 140 text

140 たった5⼈でサービスを落とす⽅法 ConsumerはSync実装、Async実装の2種類 Sync実装ではスレッドで動作 Async実装ではコルーチンで動作

Slide 141

Slide 141 text

141 たった5⼈でサービスを落とす⽅法 ConsumerはSync実装、Async実装の2種類 Sync実装ではスレッドで動作 Async実装ではコルーチンで動作 重い処理がConsumer内にあった その処理が重なるとサーバごとハングして死

Slide 142

Slide 142 text

142 SyncをAsyncに全⾯書き換え

Slide 143

Slide 143 text

143 SyncをAsyncに全⾯書き換え 各メソッドを async defに書き換え ORM周りをdatabase_to_asyncでラップ

Slide 144

Slide 144 text

144 SyncをAsyncに全⾯書き換え 各メソッドを async defに書き換え ORM周りをdatabase_to_asyncでラップ Sync実装の20倍でもサーバがハングしなくなる

Slide 145

Slide 145 text

145 静かな凡ミス

Slide 146

Slide 146 text

146 静かな凡ミス async def add_card(self, content): """ カードの追加 """ pipe_line_id = content['pipeLineId'] card_title = content['cardTitle'] database_sync_to_async(kanban_sv.add_card)( pipe_line_id, card_title ) self.broadcast_board_data() 間違い探し

Slide 147

Slide 147 text

147 静かな凡ミス async def add_card(self, content): """ カードの追加 """ pipe_line_id = content['pipeLineId'] card_title = content['cardTitle'] await database_sync_to_async(kanban_sv.add_card)( pipe_line_id, card_title ) await self.broadcast_board_data() 間違い探し

Slide 148

Slide 148 text

148 静かな凡ミス 実⾏してもエラーが出ず サイレントに処理されないので気が付かない

Slide 149

Slide 149 text

149 突然の My SQL Server has gone away

Slide 150

Slide 150 text

150 突然のMySQL Server has gone away async def hoge(self, event): # カードを取得 card = await database_sync_to_async(Card.objects.get)(id=1) # カードが所属するボードを取得して、その全体のデータを取得 board_data = await database_sync_to_async(kanban_sv.get_board_data)( card.board ) 間違い探し

Slide 151

Slide 151 text

151 突然のMySQL Server has gone away async def hoge(self, event): # カードを取得 card = await database_sync_to_async(Card.objects.get)(id=1) # カードが所属するボードを取得して、その全体のデータを取得 board_data = await database_sync_to_async(kanban_sv.get_board_data)( card.board ) 間違い探し FKをたどるだけでも、ORMの操作になる。 database_sync_to_asyncで囲わないと 接続がリークして、やがて死ぬ

Slide 152

Slide 152 text

152 突然のMySQL Server has gone away http://www.denzow.me/entry/2018/07/30/000955

Slide 153

Slide 153 text

153 あれ、Websocketで成功・失敗って・・・

Slide 154

Slide 154 text

154 あれ、Websocketで成功・失敗って・・・ 例えばカード追加がたまに失敗するシステム 成功・失敗毎にクライントにAlert出したい await KanbanClient.deleteCard({ boardId, cardId, }).then(res => { alert('success'); }).catch(e => { alert('error'); }); 成功時 失敗時

Slide 155

Slide 155 text

155 あれ、Websocketで成功・失敗って・・・ WebsocketはPromiseではない 返答を待つという概念はない socket.sendObj({ type: 'add_card', pipeLineId, cardTitle, }); このメッセージの処理状況を 容易に管理できない

Slide 156

Slide 156 text

156 どうしたのか 更新部分はAjaxで処理 処理完了時に、クライアントがブロードキャストを依頼 ローディング、成功失敗時の通知等が実装可能に

Slide 157

Slide 157 text

157 あれ、Websocketで成功・失敗って・・・ async updateCardTitle({ commit, dispatch }, { boardId, cardId, title }) { const cardData = await KanbanClient.updateCardData({ boardId, cardId, title, }); dispatch('broadcastBoardData'); }, : broadcastBoardData({ getters }) { console.log('call broadcastBoardData'); const socket = getters.getSocket; socket.sendObj({ type: 'broadcast_board_data', }); },

Slide 158

Slide 158 text

158 あれ、Websocketで成功・失敗って・・・ async updateCardTitle({ commit, dispatch }, { boardId, cardId, title }) { const cardData = await KanbanClient.updateCardData({ boardId, cardId, title, }); dispatch('broadcastBoardData'); }, : broadcastBoardData({ getters }) { console.log('call broadcastBoardData'); const socket = getters.getSocket; socket.sendObj({ type: 'broadcast_board_data', }); }, Ajaxでカードを 更新(await)

Slide 159

Slide 159 text

159 あれ、Websocketで成功・失敗って・・・ async updateCardTitle({ commit, dispatch }, { boardId, cardId, title }) { const cardData = await KanbanClient.updateCardData({ boardId, cardId, title, }); dispatch('broadcastBoardData'); }, : broadcastBoardData({ getters }) { console.log('call broadcastBoardData'); const socket = getters.getSocket; socket.sendObj({ type: 'broadcast_board_data', }); }, Ajaxが終わったら 別の処理を発⽕

Slide 160

Slide 160 text

160 あれ、Websocketで成功・失敗って・・・ async updateCardTitle({ commit, dispatch }, { boardId, cardId, title }) { const cardData = await KanbanClient.updateCardData({ boardId, cardId, title, }); dispatch('broadcastBoardData'); }, : broadcastBoardData({ getters }) { console.log('call broadcastBoardData'); const socket = getters.getSocket; socket.sendObj({ type: 'broadcast_board_data', }); }, websocket経由で consumerに ブロードキャストを依頼

Slide 161

Slide 161 text

161 ASGIサーバなのにChannels動かんやん (2018年6⽉頃)

Slide 162

Slide 162 text

162 メジャーなASGIサーバ https://github.com/django/daphne

Slide 163

Slide 163 text

163 メジャーなASGIサーバ https://github.com/encode/uvicorn

Slide 164

Slide 164 text

164 メジャーなASGIサーバ https://gitlab.com/pgjones/hypercorn

Slide 165

Slide 165 text

165 ASGIサーバなのにChannels動かんやん Django Channelsがちゃんと動いたのはdaphneのみ uvicornはAuthMiddlewareで死亡 Hypercornはなんでか忘れたけど死亡

Slide 166

Slide 166 text

166 ASGIサーバなのにChannels動かんやん Django Channelsがちゃんと動いたのはdaphneのみ uvicornはAuthMiddlewareで死亡 Hypercornはなんでか忘れたけど死亡 とりあえずChannelsなら daphne使っておけば安⼼(?)

Slide 167

Slide 167 text

まとめ

Slide 168

Slide 168 text

168 Channels⼤好き! ⾟い話が最後続いたけど、Channelsは使いやすくていい! Websocketが書きやすくていい! 2.xのナレッジ少ないのでみんなでOutput & Followしていこう ASGIでひろがるPythonに期待

Slide 169

Slide 169 text

169 Pythonやりたい⼈ https://bit.ly/2NPWCc8