MilkcocoaでBaaSを体験!~バックエンドの仕組みと使い方~

第3回MilkcocoaでGUIを作ろう!

前回は簡単にMilkcocoaでチャットアプリケーションを作りました。今回はそれを拡張する形で、実装していきたいと考えています。また同時にMVCの設計を紹介しながら構築していきます。

SPAによるフロントエンド設計の重要性

MilkcocoaなどによりBaaSのWebアプリケーションを作る際は、フロントエンドやクライアントサイドが肥大化します。基本的にBaaSは汎用的な機能を提供し、ビジネスロジック等はクライアントサイドで実装する形になる場合が多く、またそれにより、サーバでレンダリングしていたものをクライアントサイドのJSでDOMを動的に構築したり、他にもクライアントサイドのキャッシュなども重要になり、コード量が多くなります。

SPA(シングルページアプリケーション)を構築する際、その設計をしておかないと地獄が待っているわけです。jQueryなどを使いよくCSSやJS設計もせずに構築すると、コードが肥大するにつれてプロジェクトがぐちゃぐちゃな状態になり、保守が難しくなる、という状況になります。実際にSPA化により、保守が非常に大変になったプロジェクトをよく聞きます。特に設計スキルは無くとも、マークアップとちょっとしたjQueryが書けるというようなデザイナーが、SPAに挑戦しようとするときによく起ります。

そこで設計が非常に大切になります。SPAはすなわち、WebブラウザのGUIになります。GUIというのは、昔からMVCやMVVMなどのデザインパターンがあります。そのためSPAを設計をするときには、MVCなどの設計を覚えておくと良いというわけです。今回はそのMVCの設計について紹介しながら作って行きます。

MVCとは?

MVCについて説明する前に、MVCと言うものにはいくつか種類があるということを言及しておきます。GUIから来たMVCと、WebのMVCがあります。今回はどちらかと言えばGUIのMVCのことになります。またGUIのMVCも様々なパターンがあり、今回筆者が話すのはその内の一つのことについてです。

MVCとはGUIアプリケーションを、モデル、ビュー、コントローラに切りわけて設計するデザインパターンです。モデルは簡単にはアプリケーションのデータとその振る舞い、ビューが見た目、コントローラはユーザからの操作について、分けて設計していきます。

図1 MVCモデルの一例
図1 MVCモデルの一例

チャットアプリケーションをMVC設計にする

チャットアプリケーションの拡張

チャットアプリケーションを拡張して行きます。以下の機能を追加します。

  • メッセージ削除機能

まずこれについて前回と同様にレイアウトを構成してnobackendで実装しておき、それをMilkcocoaの通信に置き換えます。

レイアウトを構築する

まず簡単にレイアウトを構築しましょう。

layout.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="chat-board">
        <div id="chat-content-from">
            <input id="chat-room-text">
            <button>メッセージ入力</button>
        </div>
        <div>
            <p>こんにちは!</p>
            <button>削除</button>
        </div>
        <div>
            <p>どうもはじめまして!</p>
            <button>削除</button>
        </div>
        <div>
            <p>やぁやぁ</p>
            <button>削除</button>
        </div>
        <div>
            <p>どもども</p>
            <button>削除</button>
        </div>
    </div>
</body>
</html>

本来であればBEMやSMACSと言ったCSS等の設計も必要なのですが、分量が多くなりすぎるため今回は特に記しません。

layout.htmlの表示は以下のようになるはずです。

図2 layout.htmlを表示したところ
図2 layout.htmlを表示したところ

モデルを書く

画面のレイアウトができたところで、モデルを書いて行きましょう。

chatModel.js
(function(global){
    // プロトタイプ継承用のモデル
    // addViewで、通知するViewを設定
    // notifyにより、Viewに通知
    var Model = function(){};

    Model.prototype.addView = function(view){
        this.view = view;
    }

    Model.prototype.notify = function(){
        if( this.view != null ){
            this.view.update();
        }
    }

    var ChatElement = function(text){
        this.text = text;
        this.id = Date.now();
    }

    ChatElement.prototype = new Model();

    ChatElement.prototype.getText = function(){
        return this.text;
    }

    var Chat = function(){
        this.elements = [];
        this.id = Date.now();   
    }

    Chat.prototype = new Model();

    Chat.prototype.getName = function(){ return this.name; }

    Chat.prototype.getElements = function(){ return this.elements; }

    Chat.prototype.add = function(chatElement){
        this.elements.push(chatElement);
        this.notify();
    }

    Chat.prototype.remove = function(id){
        var changed = false;
        for(var i in this.elements){
            if( this.elements[i].id === id ){
                this.elements.splice(i,1);
                changed = true;
            }
        }
        if( changed ){
            this.notify();
        }
    }

    global.Chat = Chat;
    global.ChatElement = ChatElement;   
}(window));

重要な点はModelが変更されたときに、Viewに通知するところです。その仕組みを詳しく読んでみてください。

Modelが書けたらテストコードを作成しましょう。

chatModelTest.js
// Modelテスト用のViewモック
var ViewMock = function(){
    this.call_update_counter = 0;
}

ViewMock.prototype.update = function(){
    this.call_update_counter++;
}

ViewMock.prototype.getCounter = function(){
    return this.call_update_counter;
}

// 簡易assert関数。
function assertEqual(msg,expect,value){
    if( expect === value  ){
        showMessage(msg + " is ok! ");
    }else{
        showMessage(msg + " is failed! Expect is " + expect + " but value is " + value );
    }
}

function showMessage(text){
    var p = document.createElement("p");
    p.appendChild(document.createTextNode(text));
    document.body.appendChild(p);
    document.body.appendChild(document.createElement("br"));
}


// modelテスト

var chat = new Chat("sample");
var chatElement1 = new ChatElement("hi!")
var chatElement2 = new ChatElement("how are you?");
var chatElement3 = new ChatElement("I'm too bad!");
var viewMock = new ViewMock();

chat.addView(viewMock);
chat.add(chatElement1);
chat.add(chatElement2);
chat.add(chatElement3);


assertEqual("chat add function test ", chat.getElements()[0], chatElement1);
assertEqual("chat add function mock test ", viewMock.getCounter(), 3);

chat.remove(chatElement2.id);
assertEqual("chat remove function test", chat.getElements()[1], chatElement3);
assertEqual("chat remove function mock test ", viewMock.getCounter(), 4);
chatModelTest.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script src="chatModel.js"></script>
    <script src="chatModelTest.js"></script>
</body>
</html>

chatModelTest.htmlをWebブラウザで開くとテストコードが実行されます。これでモデルの振る舞い自体は保証できました。

ビューとコントローラを書く

まずはViewを書きましょう。

chatView.js
(function(global){
    // DOM構築のための、簡易関数
    function E(name,children){
        var elm = document.createElement(name);
        for(var i in children ){
            elm.appendChild(children[i]);
        }
        return elm;
    }

    function Text(nodeValue){
       return document.createTextNode(nodeValue);
    }

    var ChatFormView = function(){
        this.input = E("input",[]);
        this.button = E("button",[Text("メッセージを入力")])
        this.node = E("div",[
            this.input,
            this.button
        ]);
    }

    var ChatElementView = function(element){
        this.elementId = element.id;
        this.removeButton = E("button",[Text("削除")]);
        this.node = E("div",[
            E("p",[Text(element.getText())]),
            this.removeButton
        ]);
    }


    var ChatView = function(model){
        this.form = new ChatFormView();
        this.chatContentDiv = E("div",[]);
        this.node = E("div",[
            this.form.node,
            this.chatContentDiv
        ]); 
    }

    ChatView.prototype.setChatModel = function(chatModel){
        this.chatModel = chatModel;
        chatModel.addView(this);
    }

    ChatView.prototype.setController = function(controller){
        this.controller = controller;
    }

    ChatView.prototype.update = function(){
        for (var i = this.chatContentDiv.childNodes.length - 1; i >= 0 ; i-- ) {
            this.chatContentDiv.removeChild(this.chatContentDiv.childNodes[i]);
        }
        var self = this;
        var elementViews = 
            this.chatModel.getElements().map(function(m){
                var chatElementView = new ChatElementView(m);
                new ChatElementController(chatElementView, self.controller);
                return chatElementView;
            });
        for(var i in elementViews){
            this.chatContentDiv.appendChild(elementViews[i].node);
        }
    }

    global.ChatView = ChatView;
}(window));

View自体の重要な点は、Modelの状態を直接変更するようなコードは書かないことです。

そして、Controllerです。

chatController.js
(function(global){
    var ChatElementController = function(view, parentController){
        var self = this;
        view.removeButton.onclick = function(e){
            parentController.removeChatElement(view.elementId);
        }
    }


    var ChatController = function(chatView,chatModel){
        this.view = chatView;
        this.model = chatModel;
        chatView.setChatModel(chatModel);
        chatView.setController(this);

        var inputForm = chatView.form.input;
        var self = this;
        chatView.form.button.onclick = function(e){
            var text = inputForm.value;
            self.addChatElement(text);
        }
    }

    ChatController.prototype.addChatElement= function(text){
        this.model.add(new ChatElement(text));
    }

    ChatController.prototype.removeChatElement = function(elementId){
        this.model.remove(elementId);
    }


    global.ChatElementController = ChatElementController;
    global.ChatController = ChatController;
}(window));

オフラインのチャットを完成させる

それではオフラインのチャットを完成させましょう。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script src="chatModel.js"> </script>
    <script src="chatView.js"></script>
    <script src="chatController.js"></script>
    <script>
        var model = new Chat();
        var view = new ChatView();
        var contoroller = new ChatController(view,model);

        document.body.appendChild(view.node);
    </script>
</body>
</html>

このindex.htmlをWebブラウザで表示すれば、オフラインの削除機能付きチャットを使うことができます。

Milkcocoaでリアルタイムチャットを完成させる

ここまで来たら実際にMilkcocoaを組み込んで、リアルタイムチャットを完成させましょう。

MVCにどの部分にMilkcocoaの通信を置けば良いのでしょうか? それはモデルです。モデルにMilkcocoaのdataStoreのリアルタイム通信を追加して行きましょう。

onlineChatModel.js
(function(global){
    var Model = function(){};

    Model.prototype.addView = function(view){
        this.view = view;
    }

    Model.prototype.notify = function(){
        if( this.view != null ){
            this.view.update();
        }
    }

    var ChatElement = function(text, id){
        this.text = text;
        this.id = id;
        if( id === undefined ){
            this.id = Date.now();
        }
    }

    ChatElement.prototype = new Model();

    ChatElement.prototype.getText = function(){
        return this.text;
    }

    var Chat = function(dataStore){
        this.elements = [];
        this.id = Date.now();
        this.dataStore = dataStore;
        var self = this;
        dataStore.on("push",function(d){
            var text = d.value.text;
            var id = d.id;
            self.elements.push(new ChatElement(text,id));
            self.notify();
        });

        dataStore.on("remove",function(d){
            var id = d.id;
            for(var i in self.elements){
                if( self.elements[i].id === id ){
                    self.elements.splice(i,1);
                    self.notify();
                    return;
                }
            }   
        });
    }

    Chat.prototype = new Model();

    Chat.prototype.getName = function(){ return this.name; }

    Chat.prototype.getElements = function(){ return this.elements; }

    Chat.prototype.add = function(chatElement){
        this.dataStore.push({
            text : chatElement.text
        });
    }

    Chat.prototype.remove = function(id){
        this.dataStore.remove(id);
    }

    global.Chat = Chat;
    global.ChatElement = ChatElement;   
}(window));

ここで、Viewに来た通知処理がいったん、dataStoreにデータをpushしたりremoveしたりしていることに気がついたでしょうか? そしてdataStoreのonメソッドにてdataStoreの状態が変更されたことを受け取り、それをnotifyすれば、全てのWebブラウザでリアルタイムに状態を更新しているというわけです。

チャットを完成させる。

最後にチャットを完成させましょう。index.htmlを以下のように修正してください。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script src="http://cdn.mlkcca.com/v0.2.8/milkcocoa.js"></script>
    <script src="onLineChatModel.js"> </script>
    <script src="chatView.js"></script>
    <script src="chatController.js"></script>
    <script>
        var milkcocoa = new MilkCocoa("https://io-pi6n3c5ul.mlkcca.com");
        var datatStore = milkcocoa.dataStore("chat");
        var model = new Chat(datatStore);
        var view = new ChatView();
        var contoroller = new ChatController(view,model);

        document.body.appendChild(view.node);
    </script>
</body>
</html>

これでチャットアプリケーションは完成しました。

まとめ

第3回は、MVCを実践しながらチャットアプリケーションに機能追加をして行きました。BaaSを利用するアプリケーションはクライアント側でのGUIがリッチになることが多いため、しっかり責務を分割して管理することが大切になります。

おすすめ記事

記事・ニュース一覧