Lento con forza

大学生気分のIT系エンジニアが色々書いてく何か。ブログ名決めました。

Firebase Realtime DatabaseをRedux-Sagaでリアルタイムに更新する

この記事はFirebase Advent Calendar 2017 8日目の記事です。

昨日は @yamacraft さんの Firebaseプロジェクトのデプロイについて でした。アプリ開発のノウハウはたくさんあふれていますが、デプロイについては後回しにされてしまうことが多いですよね。
デプロイされないとサービスを作っても全く意味がないのでデプロイメントは非常に大切なものだと感じます。
ただ今回はアプリ開発について書きます。

ちなみにFirebase Advent Calendarには f:id:kouki_dan:20171207190511p:plain
と書いていたのですが、間に合いませんでした。1 本当はAdvent Calendarでアクセス数稼いでやろう〜〜という魂胆だったのですが、間に合わなかったのでサービスはまた今度出そうと思います。2 今回はそのサービス開発をしていく上でちょっと悩んだことについての記事を書きます。

Firebase Realtime DatabaseをRedux-Sagaでリアルタイムに更新する

はじめに

JavaScriptのアプリ、皆さん作られているでしょうか。僕は最後に作ったWebフロントはサーバー側のテンプレートエンジンでガーーッとHTMLを描画し、あとは気合でjQueryインタラクティブなページを作っていくというものでした。その後しばらくWebフロントを書く機会もなくiOSアプリを書きながら生きてきました。サービス開発したい欲がまたでてきたので、Webフロント界隈のキャッチアップを始めたのですが、あまりの変わりように驚きました。どうやら今はWebフロントとサーバーサイドを完全に分離して開発を進めていくようです。そのために色々な技術があり、様々なアーキテクチャがひしめき合っています。Firebaseを使ったJavaScriptアプリを作る場合もその通りつくらなくてはいけないため、情報のキャッチアップを行いました。今回は React + Redux + Redux-Saga をメインに使い、WebpackやBabelなどを使ってビルドを行いFirebase hostingでアセットの配信を行うことにしました。 API部分は全てFirebaseのRealtime DatabaseやFirestoreを使っています。

Redux-Saga

ReduxではAPI呼び出し等の副作用は全てMiddlewareに押し込む方針が取られているようです。僕はMiddlewareのライブラリとしてRedux-Sagaを選択しました。Redux-Sagaについての説明は [redux-sagaで非同期処理と戦う] という記事がわかりやすいのでおすすめです。

Redux-Sagaでリアルタイム通信を使う

メッセージ等のアプリを作るためには、Server Sent EventsやWebSocket等でリアルタイムにサーバーからデータを受信する実装をするのが効率的です。Firebaseにも内部的にはWebSocketが使われている、リアルタイムで更新可能なAPIが存在していて、Firebaseのドキュメントで説明されています。以下のようにデータの購読を行います。

var messageRef = firebase.database().ref('messages/' +messageId);
starCountRef.on('value', function(snapshot) {
  console.log(snapshot.val());
});

この例では messages/:postId に変更があった場合にコールバック関数が呼ばれ、snapshot.val()でその値を参照できます。このような処理はReduxではMiddlewareに書くべきであり、Redux-Sagaを使ってる場合はここに書かれるべきなのですがこのまま使うことはできないので少し工夫が必要になります。

eventChannelを使う

Redux-Sagaには色々な問題を解決するためにタスクというものが用意されています、Redux-SagaのタスクとRedux-Saga以外で発生するイベントを繋ぐことができるものにeventChannelというものがあります。これはまさに今回の用途にピッタリなのでこちらを使っていきます。

使用例

早速eventChannelを使ってRedux-SagaとFirebase Realtime Databaseを繋いでいきましょう。
まずはイベントを受信する部分の関数を見ていきます。

function *messageChannel(messageId, firebase) {
    return eventChannel(emit => {
        const messageRef = firebase.database().ref(`/messages/${messageId}`);
        messageRef.on("child_added", (snapshot) => {
            let val = snapshot.val();
            val.key = snapshot.key;
            emit(val);
        });

        const unsubscribe = () => {
            messageRef.off()
        };

        return unsubscribe
    })
}

Firebaseのrefに対してlistenを行い、イベントが発火した時点でemitを使いイベントを伝えます。このmessageChannelをcallした結果をtakeすることで、Firebase Realtime Databaseに変更があった時の処理を記述することができるようになります。コードを見たほうが早いと思うのでこちらが実際のコードです。

function *watchMessages(messageId, firebase) {
    const messages = yield call(messageChannel, messageId, firebase);
    try {
        while(true) {
            const message = yield take(messages);
            yield put({ type: "MESSAGE_ADDED", message })
        }
    } finally {
        if (yield cancelled()) {
            messages.close()
        }
    }
}

eventChannel内でemitが発行された時に、こちらのtakeが呼ばれます。この例ではemitされて届いたメッセージをそのままActionとして発行しています。おそらく誰かがこのActionを拾っていい感じに処理をしてくれることでしょう。このように責務が完全に分担されてるところもReduxのいいところですね。

あとは、任意のタイミングでSubscribeとUnsubscribeのActionを発行することでwatchMessagesを呼ぶ必要があります。

function *messagesSubscriber(roomTasks, firebase) {
    while(true) {
        const action = yield take("SUBSCRIBE_MESSAGE");
        const task = yield fork(watchMessages, action.roomKey, firebase);
        roomTasks[action.roomKey] = task;
    }
}

function *messagesUnsubscriber(roomTasks) {
    while(true) {
        const action = yield take("UNSUBSCRIBE_MESSAGE");
        if(roomTasks[action.roomKey]) {
            yield cancel(roomTasks[action.roomKey]);
        }
    }
}

subscribe時に値を入れておき、unsubscribe時にはそれをcancelするような処理を書いています。 実際に使われる時はcomponentDidMount等でSUBSCRIBE_MESSAGEアクション、componentDidUnmountでUNSUBSCRIBE_MESSAGEアクションを発行されるのでしょうか。これにより必要な変更のみを受け取る事ができるようになりました。

あとはこれらをMiddlewareとして登録する必要があるので、外部へと公開する必要があります。

export default function *messageProcess() {
    let roomTasks = {};
    yield fork(messagesSubscriber, roomTasks, firebase);
    yield fork(messagesUnsubscriber, roomTasks);
 }

これらを記述したファイルを作り、最終的にはmessageProcessをforkし登録することでFirebase Realtime Databaseに変更があった時にアクションの発行を行うことができます。

あとはReduxやStore等、UIに反映させればリアルタイム同期可能なアプリができあがりです。

まとめ

Redux-Sagaを使うとわかりにくい部分をまとめて、しかもわかりやすく書くことができるのでとても良いです。Actionを受けると何か変換を行いActionを発行する。各コンポーネントで考えなくてはいけないことはこれだけなので、とてもシンプルで何をやればいいかがわかりやすくなるメリットもあります。僕はまだJavaScript初心者なのですが、React + Redux + Redux-Saga の組み合わせはとてもわかりやすくおすすめです。流れの早いJS界ですが、頑張って追いつき、最高のサービスを提供していきたいと思っています。


  1. 本当はだいたいできてる(このカレンダーに追加した時点でできてた)のですが、最後の微調整と、Realtime DatabaseをFirestoreに移行する作業と、iOSの審査が通りませんでした。iOSの審査が凡ミスで3回連続くらい落ちたくらいでやる気が消えていきました・・・・・。

  2. 今年中にはやりきります。