2016.01.06

Firebase + ReactでリアルタイムWebサービスの実装


こんにちは。F.S.です。
今回はFirebaseを使ったリアルタイムWebサービスの実装について紹介します。

firebase_react_00
https://www.firebase.com/

FirebaseはモバイルまたはWebアプリケーション同士のリアルタイムなデータ同期を提供するバックエンドサービスです。2011年にスタートし、2014年にGoogleに買収されています。

特徴

Realtime Database

シンプルにすべてのデータ構造をJSONで表現します。
Firebase上で作成したアプリケーションのルートURLがJSONのルートオブジェクトに相当し、子要素に対してURLパスでアクセスすることができます。
firebase_react_02_data
各言語用に提供されるSDKを用いることで、任意のパス以下のデータをクライアントアプリケーションにリアルタイム同期することが可能となります。

各要素へのアクセス(Read/Write)はRuleで定義することができます。
firebase_react_03_rule

定義したルールによるアクセス可否はダッシュボードのシミュレーターで確認することができます。
firebase_react_04_simulator
なお、ルールではアクセスコントロールのほかにインデックス設定などもできます。

Authentication

Email/Passwordの認証のほか、Facebook、Twitter、GitHub、GoogleのOAuth認証に対応しています。また、既存のバックエンドでユーザー認証をしている場合では、バックエンドでFirebaseが提供するカスタム認証のトークンを生成することで、ユーザーに別途認証を求めることなくクライアントアプリケーションからFirebaseの認証を行うことができます。

Hosting

Webサイトのホスティングに利用することができるようです。
Nodeモジュールのfirebase-toolsを利用し、deployコマンドを実行することで手軽に手元のディレクトリにあるWebリソースをdeployすることができるようです。

料金体系

firebase_react_01_price
https://www.firebase.com/pricing.html

無料で始められます。必要な接続数、ストレージ・転送量の上限、バックアップの要否によって料金テーブルをアップグレードすることになります。お手軽ですね。

実装

簡単なチャットアプリケーションを作成してみます。
また、既存のWebサービスに組み込むことを想定して、カスタム認証を利用してみます。
firebase_react_08_caht

データの設計

messagesの下にルームIDごとのチャットメッセージを格納するようにします。
usersはルームのアクセスコントロールのために使用します。
$room_idなどの”$”付のものは、ルームのIDなどの動的な値がキーとなります。
{
    "messages": {
        "$room_id": {
            "$message_key": {
                "user_id": "ユーザーID",
                "user_name": "ユーザー名",
                "message": "チャットメッセージ",
                "timestamp": "タイムスタンプ",
            }
        }
    },
    "users": {
        "$user_id": {
            "rooms": {
                "$room_id": true  // アクセス権があるルーム
            }
        }
    }
}

ルールの設計

usersの自分のuidの中にキーが存在するルームIDのみ、チャットメッセージにアクセスできるようにしています。
{
    "rules": {
        ".read": false,
        ".write": false,

        "users": {
             ".read": false,
             ".write": false,

             "$user_id": {
                 "rooms": {
                     ".write": "auth.uid === $user_id",
                     ".read": "auth.uid === $user_id"
                 }
             }
        },
        "messages": {
            ".read": false,
            ".write": false,

            "$room_id": {
                ".read": "root.child('users/'+ auth.uid +'/rooms/'+$room_id).exists()",
                ".write": "root.child('users/'+ auth.uid +'/rooms/'+$room_id).exists()",
            }
        }
    }
}

カスタム認証トークン

我々はWebサービスのサーバサイドをPHPで開発する場合が多いので、PHPでトークンを生成する例を紹介します。
composerで firebase/token-generator をインストールし、下記のようにトークンを生成します。

$generator = new Firebase\Token\TokenGenerator('<FIREBASE_SECRET>');
$token = $generator
                ->setData(array(
                    'uid' => $user_id
                ))
                ->create();
echo $token;
デフォルトではトークンのexpiresは24時間になります。expiresなどのオプション指定はsetOprion(‘key’, ‘val’)で行います。詳細はREADMEを参照ください。
https://github.com/firebase/firebase-token-generator-php

クライアント実装


ここ最近、フロントエンドでよく使用することになったReactJSを用いて実装してみます。
ReactJSについてはD.M.さんの過去記事がありますので、初めての方はこちらも参照ください。
既存の Javascript のコードを React で書き換えてみる
React と jQuery を共存させる

FirebaseのReact用ライブラリがあるので、こちらを使ってみることにします。
https://www.firebase.com/docs/web/libraries/react/


それでは実装を見ていきます。

FirebaseのオリジナルのSDKの他に、ReactFireも読み込みます。
<script src="https://cdn.firebase.com/js/client/2.3.2/firebase.js"></script>
<script src="https://cdn.firebase.com/libs/reactfire/0.5.1/reactfire.min.js"></script>

メッセージリスト部分(上図の右半分)の実装はこのような形です。
var MessageList = React.createClass({
    mixins: [ReactFireMixin],

    propTypes: {
        user: React.PropTypes.object.isRequired,
        room: React.PropTypes.object.isRequired
    },  

    getInitialState: function() {
        return {
            room: this.props.room,
            messages: [], 
            message: "", 
        }   
    },  

    componentWillMount: function() {
        this.startSubscribe(this.state.room);
    },  

    // データ同期を開始するメソッド
    startSubscribe: function(room) {
        var token = FIREBASE_TOKEN;
        this.firebaseRef = new Firebase(room.room_url);
        // トークンによるカスタム認証を行う
        this.firebaseRef.authWithCustomToken(token, function(error, authData) {
            if (error) {
                console.log("Login Failed!", error);
            } else {
                console.log("Login Successed!");
                // 認証が成功すれば、messagesの配列にデータをバインドする
                this.bindAsArray(this.firebaseRef, "messages");
            }   
        }.bind(this));
    },  

    componentWillReceiveProps: function(nextProps) {
        if (nextProps.room.id != this.state.room.id) {
            this.unbind("messages");
            this.startSubscribe(nextProps.room);
            this.setState({room: nextProps.room, messages: []});
        }   
    },  

    // 新規メッセージを投稿するメソッド
    postMessage: function(message) {
        var message = { 
            user_id: this.props.user.id,
            user_name: this.props.user.name,
            message: message,
            timestamp: Firebase.ServerValue.TIMESTAMP
        };  
        this.firebaseRef.push(message);
    },

    render: function() {
        var messages = []; 
        this.state.messages.forEach(function(message, index) {
            messages.push(<Message key={index} message={message}/>);
        }, this);
        return (
            <div className="messageList">
                <ul>
                    {messages}
                </ul>
                <PostForm postMessage={this.postMessage}/>
            </div>
        );
    }
});

var Message = React.createClass({
    propTypes: {
        message: React.PropTypes.object.isRequired
    },

    render: function() {
        return (
            <li>
                <span>{this.props.message.user_name} </span>
                <span>[{moment(this.props.message.timestamp).format("YYYY-MM-DD HH:mm:ssZ")}]</span>
                <p>{this.props.message.message}</p>
            </li>
        );
    }
});

var PostForm = React.createClass({
    propTypes: {
        postMessage: React.PropTypes.func.isRequired
    },

    getInitialState: function() {
        return {
            messageValue: ""
        }
    },

    changeText: function(e) {
        this.setState({messageValue: e.target.value});
    },

    postMessage: function() {
        var message = this.state.messageValue.trim();
        if (message.length != 0) {
            this.props.postMessage(message);
        }
        this.setState({messageValue: ""});
        return false;
    },

    render: function() {
        return (
            <form onSubmit={this.postMessage}>
                <input className="textField" type="text" placeholder="メッセージを入力" value={this.state.messageValue} onChange={this.changeText}/>
                <input className="submit" type="submit" value="送信"/>
            </form>
        );
    },
});
21~35行目が特定ルーム内のメッセージ同期を開始する処理です。あわせてトークンによるカスタム認証も行っています。
45~54行目がメッセージを投げる処理です。
それ以外はReact特有の作法に従った描画処理です。

クライアントサイドで複雑な処理を記述することなく、容易にリアルタイムデータ同期ができることがわかります。
リアルタイムなデータ同期をサーバを作りこむことなく実現する上では、Firebaseの利用は有力な選択肢の一つになりますね。


それではまた。


次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。