はじめに
DjangoとAngularJSで利用してWebSocket通信を実装してみようと思いパッケージを探したところ、
DjangoでWebSocket通信するためのツールがすべて揃ってるSwampDragonというものがあったので
定番のTodo管理サイトを作ってみた。
swampdragon: http://swampdragon.net/
github: https://github.com/jonashagstedt/swampdragon
SwampDragonの特徴
WebSocket専用サーバーがある
DjangoのWSGIはHTTPなのでWebSocket通信できないので別サーバーを立てる必要がある。
SwampDragon経由でプロジェクト作成時には、server.py というファイルが生成され
これでサーバーを起動する。(/manage.py runsd.pyでもできる)
サーバーはWebフレームワークのTornadoが動作しており
server.pyの内部では、Connectionするために必要なURL生成して起動させている。
クライアントとサーバーの通信自体はSockJSを利用してる
server.pyのURLにRouterクラスがマッピングされていて
Routerクラスが保持してるSockJSのconnectionを利用してクライアントへSendしている。
modelのマッパー機能のがある
Djangoのmodelとredisをシリアリズさせてマッピングしてくれる。
modelのsave時にredisのPub/Subの機能でリアルタイムにクライアントに通知してくれる。
※「pub/sub」
「publish」と「subscribe」の略で日本語では「発行」と「購読」になる。
特定のチャンネルに誰かがイベントを「発行」すると、
そのチャンネルを「購読」している人すべてにそのイベントが通知される。
クライアントが利用するサービスも用意されてる
クライアントがサーバーにConnectionをはって、データを操作するサービス(js)が
すべて用意されてるので手間があまりかからない。
(サードパーティでObjective-cだけどiOS用のものもあった)
クライアントから実行できる操作
- データの一覧取得
- データの一覧取得(Pager付き)
- データの単一取得
- データの新規追加
- データの更新
- データの削除
- データの購読
- データの購読解除
接続ユーザーのセッションも保持できる
ログインした後のユーザー情報をサーバーサイドで保持できるようになるので
データ取得、更新時などのクエリにユーザーIDなども含めることが可能。
クライアント側ではユーザー情報を持つ必要はなくなる。
セッションを保持するには、SwampDragonの作者が用意してる
SwampDragon-authをパッケージをインストールする必要がある。
DjangoのSwampDragonを利用してTodo管理サイト作成する
実行環境
- Python 2.7.11
- Django 1.8.7
- SwampDragon 0.4.2.2
- SwampDragon-auth 0.1.3
ディレクトリ構成
ディレクトリ構成は以下のようになってる
appはAngularJS使っていてJSファイルが複数あったけど割愛してる
application
├── app
│ ├── index.html
│ ├── login.html
├── manage.py
├── module
│ ├── middleware.py
│ └── todo
│ ├── models.py
│ ├── routers.py
│ └── serializers.py
├── server.py
├── settings.py
├── urls.py
└── wsgi.py
settings.py
middlewareとswampdragon用の設定
INSTALLED_APPS = (
:
'swampdragon',
'module.todo',
)
MIDDLEWARE_CLASSES = (
:
'module.middleware.AuthenticationMiddleware',
)
# SwampDragon settings
SWAMP_DRAGON_CONNECTION = ('swampdragon_auth.socketconnection.HttpDataConnection', '/data')
DRAGON_URL = 'http://localhost:9999/'
urls.py
Views相当の処理はクライアントのAngularJSで行うのでindex.htmlのみのURLを用意
# -*- coding: utf-8 -*-
from django.conf.urls import include, url
from django.contrib import admin
from django.views.generic import TemplateView
urlpatterns = [
url(r'^$', TemplateView.as_view(template_name='index.html'), name='index'),
url(r'^login', 'django.contrib.auth.views.login', kwargs={'template_name': 'login.html'}, name='login'),
url(r'^logout', 'django.contrib.auth.views.logout_then_login', kwargs={'login_url': 'login'}, name='logout'),
url(r'^admin/', include(admin.site.urls)),
]
middleware.py
ログインしていないアクセスはログインページに飛ばすようにする
# -*- coding: utf-8 -*-
from django.shortcuts import redirect
class AuthenticationMiddleware(object):
def process_request(self, request):
if request.user.is_authenticated():
if request.path.startswith('/login'):
return redirect('/')
elif request.path == '/':
return redirect('login')
serializers.py
フロントエンドとデータのやりとりを定義するクラス
- model: モデルを「モジュール名.モデルクラス名」で定義
- publish_fields: クライアントへ通知するカラムの定義
- update_fields: クライアントから更新可能なカラムを定義
from swampdragon.serializers.model_serializer import ModelSerializer
class UserSerializer(ModelSerializer):
class Meta:
model = 'auth.User'
publish_fields = ('username',)
class TodoListSerializer(ModelSerializer):
class Meta:
model = 'todo.TodoList'
publish_fields = ('name', 'description')
update_fields = ('name', 'description')
class TodoItemSerializer(ModelSerializer):
class Meta:
model = 'todo.TodoItem'
publish_fields = ('todolist_id', 'done', 'name', 'updated_at')
update_fields = ('todolist_id', 'done', 'name')
models.py
SelfPublishModelを継承させてsave時にPublishが通知されるようにする。
serializer_classでシリアライズ設定したクラスを定義する。
# -*- coding: utf-8 -*-
from django.db import models
from swampdragon.models import SelfPublishModel
from .serializers import TodoListSerializer, TodoItemSerializer
class TodoList(SelfPublishModel, models.Model):
serializer_class = TodoListSerializer
user_id = models.IntegerField()
name = models.CharField(max_length=100)
description = models.TextField(u'説明', blank=True, null=True)
created_at = models.DateTimeField(u'作成日時', auto_now_add=True)
updated_at = models.DateTimeField(u'更新日時', auto_now=True)
class Meta:
index_together = ['user_id', 'id']
class TodoItem(SelfPublishModel, models.Model):
serializer_class = TodoItemSerializer
user_id = models.IntegerField()
name = models.CharField(max_length=100)
todolist_id = models.IntegerField()
done = models.BooleanField(u'完了フラグ', default=False)
created_at = models.DateTimeField(u'作成日時', auto_now_add=True)
updated_at = models.DateTimeField(u'更新日時', auto_now=True)
class Meta:
index_together = ['user_id', 'id']
routers.py
WebSocket専用サーバーに登録するURL、マッピングした処理を定義するクラス
- route_name: WebSocket専用サーバーに登録されるURLのPathになる(http://localhost:9999/todo-listな感じ)
- get_initial: クライアントからデータの追加、更新、削除するときに追加で含ませるパラメーターを定義
- get_subscription_contexts: クライアントがsubscriptionしたときのチャネルに含ませるパラメーターを定義
- get_object: データを単体で要求されたときのクエリー処理(必須)
- get_query_set: データをリストで要求されたときのクエリー処理(必須)
# -*- coding: utf-8 -*-
from swampdragon import route_handler
from swampdragon.route_handler import ModelRouter
from module.todo.models import TodoList, TodoItem
from module.todo.serializers import UserSerializer, TodoListSerializer, TodoItemSerializer
class UserRouter(ModelRouter):
route_name = 'user'
serializer_class = UserSerializer
def get_object(self, **kwargs):
return self.connection.user
def get_query_set(self, **kwargs):
pass
class TodoListRouter(ModelRouter):
route_name = 'todo-list'
serializer_class = TodoListSerializer
model = TodoList
def get_initial(self, verb, **kwargs):
kwargs['user_id'] = self.connection.user.id
return kwargs
def get_subscription_contexts(self, **kwargs):
# 更新通知がログインユーザーのみに送られるようユーザーIDを入れてユニークなチャネルを作る(todolist|user_id:1なチャネルになる)
kwargs['user_id'] = self.connection.user.id
return kwargs
def get_object(self, **kwargs):
user_list = self.model.objects.filter(id=kwargs['id'], user_id=self.connection.user.id)
return user_list[0] if user_list else None
def get_query_set(self, **kwargs):
user_id = self.connection.user.id
return self.model.objects.filter(user_id=user_id)
class TodoItemRouter(ModelRouter):
route_name = 'todo-item'
serializer_class = TodoItemSerializer
model = TodoItem
def get_initial(self, verb, **kwargs):
kwargs['user_id'] = self.connection.user.id
return kwargs
def get_subscription_contexts(self, **kwargs):
kwargs['user_id'] = self.connection.user.id
return kwargs
def get_object(self, **kwargs):
user_list = self.model.objects.filter(id=kwargs['id'], user_id=self.connection.user.id)
return user_list[0] if user_list else None
def get_query_set(self, **kwargs):
user_id = self.connection.user.id
return self.model.objects.filter(user_id=user_id)
route_handler.register(UserRouter)
route_handler.register(TodoListRouter)
route_handler.register(TodoItemRouter)
クライアント
クライアントはAngularJS側を利用する
$dragonがSwampDragonが用意してるサービス。
angular.module('todoApp')
.controller('PageController', ['$scope', '$dragon', '$dataHandler',
function ($scope, $dragon, $dataHandler) {
$scope.todoListChannel = 'todoListClient';
$scope.todoItemChannel = 'todoItemClient';
// 初回ページアクセス時
$dragon.onReady(function() {
// todolist, todoItemの情報を購読する(変更通知されるようなる)
$dragon.subscribe('todo-list', $scope.todoListChannel, {}).then(function(response) {
$scope.TodoListMapper = new DataMapper(response.data);
});
$dragon.subscribe('todo-item', $scope.todoItemChannel, {}).then(function(response) {
$scope.todoItemMapper = new DataMapper(response.data);
});
// todolist, todoItem, userのデータを取得
$dragon.getSingle('user', {}).then(function(response) {
$dataHandler.user = response.data;
});
$dragon.getList('todo-list', {list_id: 1}).then(function(response) {
$dataHandler.todoLists = response.data;
});
$dragon.getList('todo-item', {list_id: 1}).then(function(response) {
$dataHandler.todoItems = response.data;
});
});
// todolist, todoItemでsaveがあった場合の変更通知
$dragon.onChannelMessage(function(channels, message) {
if (indexOf.call(channels, $scope.todoListChannel) > -1) {
$scope.$apply(function() {
$scope.TodoListMapper.mapData($dataHandler.todoLists, message);
});
}
if (indexOf.call(channels, $scope.todoItemChannel) > -1) {
$scope.$apply(function() {
$scope.todoItemMapper.mapData($dataHandler.todoItems, message);
});
}
});
}]);
起動
python ./manage.py runserver
python server.py
まとめ
サーバーサイドはすごく簡単にでき、Djangoをさわったことがある人には馴染みやすいと思う。
ただ、クライアント側はリアルタイムでHTMLへ画面更新させるJSのコールバックなどの処理が
がんばらないといけなさそうな雰囲気。
AngularJSを利用するとHTMLとJS変数のデータバインディングができるので画面更新の手間は改善できそう。
日本ではあまり導入実績はなさそうだけど、githubを見ると
ちらほら使われているようなので今後に期待したい。
今回作成したTodo管理サイトのコードはgithubに置いてます.
https://github.com/fujimisakari/todo-server