Firebaseを始めよう No.2 Realtime Databaseを使ったチャット

この記事の完全なソースコードは次のリポジトリのpublic/dbフォルダにあります。

GitHub - webarata3/begin_Firebase: Firebaseのテストです。

プロジェクトの準備

今回は、FirebaseのRealtime Databaseを使って簡単なチャットを作っていきます。

Realtime Databaseを使うためには、Fireabaseのコンソールから「Database」→「データベースを作成」を選択します。

ダイアログが出てくるので、「テストモードで開始」を選びます。

説明のためこのような設定をしますが、誰でもどのデータでも読み取り、書き込み、変更ができるため本番環境では適切な設定をしてください。

プログラム

次のような簡単なチャットプログラムでRealtime Databaseの動作を確認します。

最初にコードの全体を確認します。

Cutom Elementsで入力フィールドを作っているので、そのfloting-box.jsを最初に確認します。

'use strict';

class FloatingBox extends HTMLElement {
  constructor() {
    super();
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
<style>
* {
  box-sizing: border-box;
}

/* https://www.pc-weblog.com/floating-label/ */

:host {
  --font-size: 14px;

  position: relative;
  margin-top: calc(var(--font-size) - 9px);
  margin-bottom: 20px;
  border: 1px solid #666;
  display: inline-block;
}

input {
  padding: calc(var(--font-size) - 5px) 6px 8px;
  font-size: var(--font-size);
  border: none;
  outline: none;
  box-sizing: border-box;
  width: 100%;
}

label {
  position: absolute;
  top: 50%;
  left: 10px;
  font-size: calc(var(--font-size) - 2px);
  color: #666;
  transform: translate(0, -50%);
  cursor: text;
}

input:focus + label, input:not(:placeholder-shown) + label {
  top: 0;
  left: 10px;
  font-size: calc(var(--font-size) - 4px);
  transition: 0.5s;
  background: #fff;
  color: #1869fe;
  padding: 0 0.2rem;
}

input:not(:placeholder-shown) + label {
  color: #666;
}

/* 入力中のみ青色 */
input:focus + label {
  color: #1869fe;
  font-weight: bold;
}
</style>
<input type="text" placeholder="&nbsp;">
<label></label>
`;

    this.style.setProperty('--font-size', '14px');

    // ラベルをクリックしたときにフォーカスが当たるようにする。
    const input = shadowRoot.querySelector('input');

    this.addEventListener('click', event => {
      input.focus();
    });
  }

  static get observedAttributes() {
    return ['label', 'size', 'font-size'];
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    switch (attrName) {
      case 'label':
        this.shadowRoot.querySelector('label').textContent = newVal;
        break;
      case 'font-size':
        const fontSize = newVal || '14';
        this.style.setProperty('--font-size', fontSize + 'px');
        break;
      case 'size':
        this.shadowRoot.querySelector('input').setAttribute('size', newVal);
        break;
    }
  }
}

customElements.define('floating-box', FloatingBox);

HTMLは次のようにします。

<!DOCTYPE html>
<html lang="ja">
 <head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>チャット</title>
  <link href="/db/chat.css" rel="stylesheet">
 </head>
 <body>
  <main>
   <div>
    <floating-box id="name" size="10" font-size="14" label="名前"></floating-box>
    <floating-box id="message" size="50" font-size="14" label="メッセージ"></floating-box>
   </div>
   <div id="chatList"></div>
  </main>
  <template id="messageTemplate">
   <div class="chat">
    <div class="name"></div>
    <div class="message"></div>
   </div>
  </template>
  <script src="/__/firebase/5.4.2/firebase-app.js"></script>
  <script src="/__/firebase/5.4.2/firebase-database.js"></script>
  <script src="/__/firebase/init.js"></script>
  <script src="/common/floating-box.js"></script>
  <script src="/db/chat.js"></script>
 </body>
</html>

Realtime Databaseを使うときは、「firebase-database.js」を読み込むようにします。

次にCSSです。

@charset "utf-8";

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
}

main {
  margin: 5px;
}

.name {
  color: #999;
  font-size: 7px;
}

.chat {
  position: relative;
}

.chat:hover {
  background-color: #eee;
}

.message {
  text-indent: 3em;
}

#chatList {
  margin-top: 10px;
}

#chatList > div {
  padding: 5px;
}

#chatList > div:not(:last-child) {
  border-bottom: 1px solid #999;
}

最後にJavaScriptのソースです。

'use strict';

const nameEl = document.getElementById('name').shadowRoot.querySelector('input');
const messageEl = document.getElementById('message').shadowRoot.querySelector('input');
const chatList = document.getElementById('chatList');

const messagesRef = firebase.database().ref('chat');

const registerMessage = event => {
  if (event.key === 'Enter') {
    const name = nameEl.value;
    const message = messageEl.value;
    if (name && message) {
      messagesRef.push({name: name, message: message});
      messageEl.value = '';
    }
  }
};

nameEl.addEventListener('keypress', registerMessage);
messageEl.addEventListener('keypress', registerMessage);

messagesRef.on('child_added', data => {
  const message = data.val();

  const template = document.getElementById('messageTemplate');
  template.content.querySelector('.name').textContent = message.name;
  template.content.querySelector('.message').textContent = message.message;

  const clone = document.importNode(template.content, true);

  chatList.insertBefore(clone, chatList.firstChild);
});

データの構造

Realtime DatabaseはJSONとしてデータを格納します。Realtime Databaseのデータを操作するには参照を作成する必要があります。参照は次のように、格納されるデータのパスのようなものを指定します。

const messagesRef = firebase.database().ref('chat');

この例でいうと、ルートのchat以下にデータをぶら下げます。この参照を使ってデータを格納すると、次のようなデータ構造になります。

データの登録

データの登録は次のように行います。

messagesRef.push({name: name, message: message});

先述の通り、データはJSONとして格納するので、格納するデータはJavaScriptのオブジェクトの形式で記述します。この例では、参照に対してpushしてデータを格納しています。pushで格納するとデータベースに一意のキーが作成され、そこに指定したデータが格納されます。この例で格納したイメージは次のようになります。

pushで作成されるキーは、タイムスタンプに基づいて作成されるので、キーで並べると作成順に並びます。

更新の検知

Realtime Databaseはデータが更新されるとそれが通知されます。基本的には参照に対して操作が行われたときに、それに対応したイベントが発火されるので、それを検知して対応します。

今回の場合には、データが追加されていくので「child_added」イベントを検知して、イベントが発生した際には画面を更新します。イベントの検知は次のようなコードになります。

messagesRef.on('child_added', (data) => {
  //
});

イベント発生時に呼び出される関数の引数には、追加されたデータが渡されます。data.keyでキー、data.val()で値が取れます。

ルールと注意

Firebaseのコンソール画面から、データベースにデータを格納する際のルールが設定できます。ルールでは、例えば認証しているユーザのみ読み書きできたり、値のValidationをかけたりといったことができます。今回は次のようなルールを設定しました。

{
  "rules": {
    ".read": false,
    ".write": false,
    "chat": {
      ".read": true,
      ".write": true,
      "$messageId": {
        "name": {
          ".validate": "newData.isString() && newData.val().length > 0"
        },
        "message": {
         ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 30"
        }
      }
    }
  }
}
このルールもchat以下に誰でも書き込めるので、本番環境ではより適切にルールを設定してください。

登録できるユーザは制限していません。nameとmessageは必ず値が入っていること(0より大きい文字数)かつ、messageは30文字以下で有ることというルールにしています。

これで見かけはうまくいくのですが、1つ大きな問題があります。ここで、例えばルールに反するようにmessageを31文字入力します。そうすると、画面上はデータが追加されますが、サーバ上のデータはルールに弾かれるためデータは追加されません。もちろん、JavaScript側でもルールに違反したというエラーが取得できます。

ですが、データのチェックは非同期でされているかつ、child_addedイベントはローカルで行った場合はローカルでルール関係なく起こるような感じです(たぶん)。

そのため、きちんとしたものを作ろうとする場合にはクライアントでのvalidationが必要になると思います。

コメント