[Firebase] Firestoreでリアルタイムなチャットを作る (Web編) その1

今回はFirestoreでリアルタイムに情報のやりとりをしてみます。こういったときのサンプルは概ねチャットと相場が決まっていますので、ひとまずデータの授受ができる最低限のコードを書いてみます。

Firestoreの準備と基本的な使い方

詳しくは以下のページをご覧くださいませ。 blog.katsubemakito.net

今回はひとまず動くものを作るのを目標としますので、ユーザー管理は行わず一つの部屋にひたすらメッセージがたまっていく作りにします。

また「ルール」は今回も全開放しています。

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

サンプル

実行結果

  • ユーザー名は10種類の中からアクセスする度にランダムに決定されます(選べません)
  • 自由に投稿していただいて問題ありませんが、公序良俗に反する書き込みはご遠慮ください。
  • 適当なタイミングでお掃除します。

ソースコード

config.js

Firebaseのコンソールで表示される内容をそのままコピペしconfig.jsというファイル名で保存しました。

// コンソールの内容をそのままコピペ
var config = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "test-f76bc.firebaseapp.com",
  databaseURL: "https://test-f76bc.firebaseio.com",
  projectId: "test-f76bc",
  storageBucket: "test-f76bc.appspot.com",
  messagingSenderId: "1111111111111111"
};
firebase.initializeApp(config);

HTML/JavaScript

こちらが本体です。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>FireStore Chat</title>
  <style>
    #chatlog{ width:500px; height:300px; border:1px solid gray; overflow-y:scroll; }
    #uname{ width:80px; float:left; margin-right:10px; padding-top:5px; text-align:center}
    #msg{ width:330px; height:30px; margin-right:10px; font-size:12pt;}
    #sbmt{ width:100px; height:30px; }
  </style>
</head>
<body>
  <h1>FireStore Chat</h1>

  <!-- 発言が表示される領域 -->
  <ul id="chatlog"></ul>

  <!-- 入力フォーム -->
  <form id="form1">
    <div id="uname"></div>
    <input type="text" id="msg"><button type="button" id="sbmt">送信</button>
  </form>

  <script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.8.1/firebase-firestore.js"></script>
  <script src="/js/config.js"></script>
  <script>
    //---------------------------------------
    // チャット初期処理
    //---------------------------------------
    // ユーザー名をランダムに決める
    var uname = getUName();
    document.getElementById("uname").innerHTML = uname;

    // テキストボックスにfocus
    // document.getElementById("msg").focus();

    //---------------------------------------
    // Firestoreの準備
    //---------------------------------------
    // Firestoreのインスタンス作成
    var db = firebase.firestore();

    // チャットルームのリファレンス取得
    var messagesRef = db.collection("chatroom").doc("room1").collection("messages");

    /**
     * 同期処理
     **/
    messagesRef.orderBy("date", "asc").limit(20).onSnapshot( (snapshot) => {
      snapshot.docChanges().forEach((change) => {
        // 追加
        if ( change.type === 'added' ) {
          addLog(change.doc.id, change.doc.data());
        }
        // 更新
        else if( change.type === 'modified' ){
          modLog(change.doc.id, change.doc.data());
        }
        // 削除
        else if ( change.type === 'removed' ) {
          removeLog(change.doc.id);
        }
      });
    });

    /**
     * 送信ボタン押下
     **/
    document.getElementById("sbmt").addEventListener("click", ()=>{
      let msg = document.getElementById("msg").value;
      if( msg.length === 0 ){
        return(false);
      }
      // メッセージをfirestoreへ送信
      messagesRef.add({
        name: uname,
        msg: msg,
        date: new Date().getTime()
      })
      .then(()=>{
        let msg = document.getElementById("msg");
        msg.focus();
        msg.value = "";
      })
    });
    // submitイベントは(いったん)無視する
    document.getElementById("form1").addEventListener("submit", (e)=>{
      e.preventDefault();
    });


    /**
     * ログに追加
     */
    function addLog(id, data){
      // 追加するHTMLを作成
      let log = `${data.name}: ${data.msg} (${getStrTime(data.date)})`;
      let li  = document.createElement('li');
      li.id   = id;
      li.appendChild(document.createTextNode(log));

      // 表示エリアへ追加
      let chatlog = document.getElementById("chatlog");
      chatlog.insertBefore(li, chatlog.firstChild);
    }

    /**
     * ログを更新
     */
    function modLog(id, data){
      let log = document.getElementById(id);
      if( log !== null ){
        log.innerText = `${data.name}: ${data.msg} (${getStrTime(data.date)})`;
      }
    }

    /**
     * ログを削除
     **/
    function removeLog(id){
      let log = document.getElementById(id);
      if( log !== null ){
        log.parentNode.removeChild(log);
      }
    }


    /**
     * ユーザー名をランダムに決定
     **/
     function getUName(){
      let master = ["キティ", "マイメロ", "プリン", "ぐでたま", "烈子", "シナモン", "たあ坊", "キキ", "ララ", "切り身"];
      let i      = Math.floor( Math.random() * master.length );
      return( master[i] );
    }

    /**
     * UNIX TIME => MM-DD hh:mm
     **/
    function getStrTime(time){
      let t = new Date(time);
      return(
        ("0" + (t.getMonth() + 1)).slice(-2) + "-" +
        ("0" + t.getDate()       ).slice(-2) + " " +
        ("0" + t.getHours()      ).slice(-2) + ":" +
        ("0" + t.getMinutes()    ).slice(-2)
      );
    }
  </script>
</body>
</html>

解説

同期する

以下の例ではコレクション/chatroom/room1/messagesと同期をしています。 何らかの変更(追加、更新、削除)が発生した際にdocChanges()が実行されます。変化があったドキュメントが渡されますのでそれをforEachでぐるぐる回しながら処理をしていきます。

var messagesRef = db.collection("chatroom").doc("room1").collection("messages");

messagesRef.onSnapshot( (snapshot) => {
  snapshot.docChanges().forEach((change) => {
    // 変更があった際の処理をここに記述
  });
});

イベントの種類

forEachの中身ですが、added, modified, removedのいずれかのイベントが返ってきますので、イベントの種類に応じて処理を分岐してあげるのが良いでしょう。

  snapshot.docChanges().forEach((change) => {
    // 追加
    if ( change.type === 'added' ) {
      addLog(change.doc.id, change.doc.data());
    }
    // 更新
    else if( change.type === 'modified' ){
      modLog(change.doc.id, change.doc.data());
    }
    // 削除
    else if ( change.type === 'removed' ) {
      removeLog(change.doc.id);
    }
  });

データ量を制限する

以下のようにlimit()で返却される件数を制限することができます。同時にorderBy()でソートをかけておくと新しい物(古い物)から順番に返してくえるようになります。

messagesRef.orderBy("date", "asc").limit(20).onSnapshot( (snapshot) => {

更新と削除

削除や更新はUIを作るのが面倒だったのと、「ルール」が全開放となっている関係で今回は作成しませんでしたが、Firebaseのコンソールから試していただければ、正常に動作することを確認できると思います。  ※=ご自身で環境を作った上でお試しください

レイテンシもそれほど感じないですし、コードも非常に簡潔にかけるのがわかりますね。

続き

blog.katsubemakito.net

参考ページ

firebase.google.com