GCP 

既存のWebサイトに瞬時更新コメント欄を追加する方法
Firebaseを用いて1日で実装してみた

ちゅらデータのAustin Mayerです。
本記事では、Firebaseを活用して、既存のWebサイトに瞬時更新コメント欄を追加する方法を紹介します。

既存のWebサイトに対して、最新の技術を実装することが難しい場面があります。例えば、WebSocketを使ってチャット機能をホームページに追加したいが、既存のバックエンドはWebSocketに対応するのが難しいような状況が考えられます。

そこで、助けてくれるのは、Google Cloud PlatformのFirebaseです!

目次

Firebaseとは

Firebaseは、バックエンドの責務(データベース、API、ホスティング、ユーザー認証)を肩代わりするmBaaS (Mobile Backend as a Service)の一つで、アプリ開発のリソースや時間の大幅な節約が可能とし、コストの削減も期待できます。

Firebaseには、NoSQLデータベースのRealtime Databaseというサービスがあり、JSONのような構造的なデータを保持することが可能です。

WebアプリケーションからRealtime Databaseのデータを取得・更新したい場合、HTTP APIを使用する方法とWebSocketを使用する方法の2つがあります。
今回は後者のWebSocketを用いて、リアルタイムに反映されるコメント欄を実装していきます!

本記事で使われる技術

1.TypeScript
2.Docker
3.Ruby on Rails

擬似既存のwebsiteをセットアップ

既存のWebサイトを用意するのは難しく、Ruby on Railsを使って、Scaffoldingでブログサイトをチャチャッと作ります!
2.7.0以上のRubyをローカルにインストールする必要があるので、Rubyのバージョンをご確認ください。

Rubyのダウンロードはこちら
https://www.ruby-lang.org/ja/downloads/

Railsでホームページを作成

まずはRailsのGemをインストールします。

gem install rails

Railsでブログのサイトを作ります。ただし、webpackはインストールしません。

rails new blog --skip-webpack-install
cd blog
rails generate scaffold post title:string body:text
rake db:migrate  
rails server

そうすると、appというフォルダーにこのような自動作成のRailsアプリが出てきます!
変更するところは一つだけ!

ruby:config/routes.rb
Rails.application.routes.draw do
 resources :posts
 root "posts#index"
end

すると、不恰好ではありますが、このような、素朴なホームページになります。

Dockerfileを作成

プロジェクトのルートダイレクトリにDockerfileを追加し、RailsアプリをDocker化します。

Dockerのドキュメントにあるテンプレートを参考にします。
https://docs.docker.com/samples/rails/

touch Dockerfile
touch entrypoint.sh

テンプレートを変更する箇所が複数ありました。

Dockerfile
FROM --platform=arm64 ruby:3.0.0
RUN apt-get update && apt-get install -y postgresql-client libc6
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
 
ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
 
# ARM対策
RUN gem install nokogiri --platform=ruby
RUN bundle config set force_ruby_platform true
 
RUN bundle install
 
COPY . /app/
 
RUN rails db:migrate
RUN rails assets:precompile
 
EXPOSE 3000
VOLUME [ "/app/log" ]
 
CMD ["rails", "server", "-e", "production", "-b", "0.0.0.0"]

ちなみに、筆者はM1 Macで開発しておりますので、上記のDockerfileはarm64のプラットフォームに限定しています。

PostgreSQLのデータベース設定

次はPostgreSQLのデータベースが使えるようにします。

BundleでpgというPostgreSQLのGemを追加

ローカル環境にPostgreSQLをインストールしていないと、Bundleがエラーになるのでインストールしてください。
Macではbrewで簡単にインストールできます。

brew install postgresql

PostgreSQLがインストールされたら、次はRailsアプリのダイレクトリで以下のコマンドを実行します。

bundle config set --local path 'vendor/bundle'
bundle add pg

これでGemfileを除くと、

最後の方にpgが入りました。また、Gemfile.lockも変わっています。これでOKです!

config/database.ymlの修正

database.ymlのデフォルトの設定を以下のようにpostgresに変えます。

default: &default
 adapter: postgresql
 encoding: utf8
 database: <%= ENV["DB_NAME"] %>
 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
 host: <%= ENV["DB_HOST"] %>
 username: <%= ENV["DB_USER"] %>
 password: <%= ENV["DB_PASS"] %>
 timeout: 5000
 
development:
 <<: *default
 
test:
 <<: *default
 
production:
 <<: *default

環境変数でデータベース接続情報を与えるので、Railsの機能に頼りましょう!

Dockerfileの更新

MySQLiteを使っていないので、ビルドの過程でdb:migrateを実行するのはNGです。DBの準備操作は、ビルドして、コンテナが起動してから、別途しなければなりません。

FROM --platform=arm64 ruby:3.0.0
RUN apt-get update && apt-get install -y nodejs postgresql-client libc6
WORKDIR /app
COPY Gemfile Gemfile.lock /app/
 
ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
 
# ARM対策
RUN gem install nokogiri --platform=ruby
RUN bundle config set force_ruby_platform true
 
RUN bundle install
 
COPY . /app/
 
RUN rails assets:precompile
 
EXPOSE 3000
VOLUME [ "/app/log" ]
 
CMD ["rails", "server", "-e", "production", "-b", "0.0.0.0"]

rails-docker.envの環境変数ファイルの作成

docker runを実行する時に、これを–env-fileで指定します。

.env
DB_HOST=host.docker.internal
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=root

Postgresのコンテナを起動

Docker HubのPostgres公式イメージを使います!
https://hub.docker.com/_/postgres

docker run --name rails-postgres -p 5432:5432 -e POSTGRES_PASSWORD=root -d postgres

Railsコンテナを立ち上げ、DBをMigrate

次はRailsアプリのコンテナを起動させましょう。
上記で作ったrails-docker.envの環境変数ファイルを使います。

docker run --rm -p 3000:3000 --env-file rails-docker.env tronicboy/firebase-comments:pg

これだと、データベースのテーブルが作成されていないので、rails db:migrateを実行する必要があります。
docker ps でRailsアプリのコンテナIDを取得して、それを使ってバッシュに入ります。

docker ps
docker exec -it <コンテナID> /bin/bash
rails db:migrate

これで、全くパッとしない、擬似既存Webサイトは完成!

既存のwebsiteにReactを入れる

これからはどのプロジェクトでもできるようにwebpackの設定も含めてReactをセットアップします。

webpackをセットアップ

webpackの公式ドキュメントを参考に作業します。
https://webpack.js.org/guides/typescript/

まず、npmもしくはyarnでプロジェクトのルートダイレクトリーにpackage.jsonを入れてもらいます。
筆者はyarnが好みなので、以下yarnを使います!

yarn init

各設定を任意の値にしてください。ひたすらEnterを連打するのでもいいです。

するとこのようにpackage.jsonが出てきます。
次はwebpackをインストールします。

yarn add -D webpack webpack-cli typescript ts-loader css-loader style-loader

続いてReactのソースを入れるフォルダを作ってindex.tsxをそこに入れます。

mkdir src
touch src/index.tsx

次に、webpackとtypescriptの設定ファイルも追加します。

touch webpack.config.js
touch tsconfig.json

そこに以下のような設定を入れます。

javascript:webpack.config.js
const path = require("path");
 
module.exports = {
 entry: "./src/index.tsx",
 module: {
   rules: [
     {
       test: /\.tsx?$/,
       use: "ts-loader",
       exclude: /node_modules/,
     },
     {
       test: /\.css$/i,
       use: ["style-loader", "css-loader"],
     },
   ],
 },
 mode: "production",
 resolve: {
   extensions: [".tsx", ".ts", ".js"],
 },
 output: {
   filename: "bundle.js",
   path: path.resolve(__dirname, "public/js"),
 },
};

tsconfig.jsonは以下のように変更します。「allowSyntheticDefaultImports」をtrueにすることで、Reactのパッケージをインポートするために必要です。

json:tsconfig.json
{
 "compilerOptions": {
   "outDir": "./dist/",
   "noImplicitAny": true,
   "allowSyntheticDefaultImports": true,
   "module": "ES2022",
   "target": "es6",
   "jsx": "react",
   "allowJs": true,
   "moduleResolution": "node"
 },
 "include": ["src/**/*"],
 "exclude": ["node_modules", "vendor"]
}

最後にpackage.jsonのscriptsを以下のようにします。

json:package.json
{
 "name": "blog",
 "version": "1.0.0",
 "main": "index.js",
 "author": "Austin Mayer",
 "license": "MIT",
 "scripts": {
   "build": "webpack --config webpack.config.js"
 },
 "devDependencies": {
   "css-loader": "^6.7.1",
   "style-loader": "^3.3.1",
   "ts-loader": "^9.2.8",
   "typescript": "^4.6.3",
   "webpack": "^5.71.0",
   "webpack-cli": "^4.9.2"
 }
}

ここでビルドが正常にできるか確認するために、index.tsxに適当なコードを入れて

そしてyarn buildを実行してpublic/jsをみると、

ちゃんと出てきてますね!

Reactをセットアップ

Reactを動かしたいのでまず最初にReactのパッケージをpackage.jsonに追加しましょう。

yarn add react react-dom @types/react @types/react-dom

そしてReactのアプリ部品を作ります。

touch src/App.tsx
typescript:src/App.tsx
import React from "react";
 
const App: React.FC = () => {
 return <h1>React, Just for You.</h1>;
};
 
export default App;

このApp.tsxをDOMにレンダーしたいので、index.tsxでreact-rootという<div>があるかを確認した上で実行します。

typescript:src/index.tsx
import ReactDOM from "react-dom"
import React from "react";
import App from "./App";
 
const domContainer = document.getElementById("react-root") as HTMLDivElement;
 
if (domContainer) {
 ReactDOM.render(<App />, domContainer)
}

次に必要なのは、この<div id=”react-root”>をRailsのERBテンプレートに追加して、bundle.jsの<script>タグを追加することです。

erb:app/views/posts/show.html.erb
<p style="color: green"><%= notice %></p>
 
<%= render @post %>
 
<div id="react-root"></div>
<%= javascript_include_tag "/js/bundle.js" %>
 
<div>
 <%= link_to "Edit this post", edit_post_path(@post) %> |
 <%= link_to "Back to posts", posts_path %>
 
 <%= button_to "Destroy this post", @post, method: :delete %>
</div>

最後に、Dockerfileを少しいじらないといけません。マルチステージビルドにして、nodeイメージでwebpackを実行して成果物のbundle.jsをrunnerのステージにコピーします。

Dockerfile
FROM node:14.18.2-alpine3.12 AS webpack-builder
WORKDIR /app
 
COPY tsconfig.json package.json yarn.lock webpack.config.js /app/
RUN yarn install
 
COPY /src /app/src
RUN yarn build
 
FROM --platform=arm64 ruby:3.0.0 AS runner
RUN apt-get update && apt-get install -y nodejs postgresql-client libc6
WORKDIR /app
COPY Gemfile Gemfile.lock /app/
 
ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
 
# ARM対策
RUN gem install nokogiri --platform=ruby
RUN bundle config set force_ruby_platform true
 
RUN bundle install
 
COPY . /app/
# WebpackでビルドしたReactの成果物を持ってくる
COPY --from=webpack-builder /app/public/js /app/public/js
 
RUN rails assets:precompile
 
EXPOSE 3000
VOLUME [ "/app/log" ]
 
CMD ["rails", "server", "-e", "production", "-b", "0.0.0.0"]

これでReactのJavaScriptを理論上、どの既存プロジェクトでもできるはず!!
既存のプロジェクトにマルチエントリポイントのwebpackがされていれば、なおさらやりやすいです。
webpackのエントリポイントの設定についてはこちらをご覧ください。
https://webpack.js.org/concepts/entry-points/

Dockerでビルドして、Reactが出ていることを確認

筆者が思っていたより長い道のりでしたが、やっとその瞬間がきました。
そうです、Dockerで果たして動くのでしょうか?

docker build -t gcp-rails:latest . 
docker run --rm -p 3000:3000 gcp-rails:latest

良さそう。

良さそう。

良さそう。

よろしい!
これで(やっと)次に進めるどー!かりゆしやっさ!

Firebaseで新規プロジェクトを作る

グーグルでFirebaseを検索してアカウントを作ってください。
無料枠でRealtime Databaseが使えるので、ご安心を!

新規プロジェクトの作成

サインアップが終わるとダッシュボードへ飛びます。「プロジェクトを追加」のリンクをクリックしてください。

某A社と違って、手順はたった三つで終わるという。

今回はアナリティクスを使用しないので無効にします。手順2つだけになりました。

Realtime Databaseの有効化

しばらく経つと新規プロジェクトが作成されるので、そのプロジェクトのダッシュボードに入ります。
左側のメニューリストからRealtime Databaseのリンクをクリックし、続いてデータベースを作成します。

設定するロケーションは最も近いシンガポールのサーバーにしましょう。

次はRealtime Databaseのセキュリティに当たる「ルール」を設定するところですが、今回はテストモードで始めます。

FirebaseのAuthentication機能を使うと、ユーザーベースでデータベースのどのダイレクトリにどのようなアクセスができるのかを細かく設定できます。

「有効にする」をクリックするとRealtime Databaseが作成され、ダッシュボードに追加されます。

ここで直接JSONの形式と同じように、キーに値を追加することができます。
既存のデータをJSON形式でインポートすることもできますし、バックアップも取れます。

ちなみに、ここに出ているURLに.jsonを追加してFetchリクエストをブラウザで送ると、このようなJSONデータが帰ってきます。

アプリの登録

次に必要なのは、我々のReactアプリでこのRealtime Databaseに接続するための情報を取得することです。Firebaseをどのアプリで使うのか登録する必要があります。

どのアプリで使うかというのは、FirebaseはiOSとAndroidにも対応しやすいようにできているので、複数のプロジェクトでも同じRealtime Databaseにアクセスができるようにしているのです。

プロジェクトの設定で認証に接続に必要な情報をゲットしましょう!

下にスクロールして、HTMLタグのようなマークをクリックします。

記録のために、アプリ名を入力します。Firebase Hostingは、Googleの方で静的ファイルをホスティングする機能で今回は使いません。

アプリを登録すると、必要な認証情報が表示されます。これはこのまま置いておきます。

ここまで来たので、Reactの方でRealtime Databaseを使った実装をしましょう!

フロントエンドのReactにfirebaseパッケージをインストールし接続

これからは以下の公式ドキュメントを参考にしてRealtime DatabaseのWebSocketを接続します!
https://firebase.google.com/docs/database/web/start

ReactでFirebaseをより使いやすくするnpmパッケージが他にもあるのですが、今回はReactバニラを使用して簡単な実装をします。

Firebase SDKをpackage.jsonに追加

上記のFirebase Consoleで出てきたnpm installのコマンドをyarn addにします。

yarn add firebase

Firebaseの基礎設定をエクスポート

srcに以下のファイルを追加します。

mkdir src/firebase
touch src/firebase/index.ts

そしてfirebase/index.tsに以下のコードを入れます。

typescript:src/firebase/index.ts
import { initializeApp } from 'firebase/app';
import { getDatabase } from "firebase/database";
 
// 必須 : Firebase Consoleで取得した認証情報をここに入れる
const firebaseConfig = {
 apiKey: "API_KEY",
 authDomain: "PROJECT_ID.firebaseapp.com",
 databaseURL: "https://DATABASE_NAME.firebaseio.com",
 projectId: "PROJECT_ID",
 storageBucket: "PROJECT_ID.appspot.com",
 messagingSenderId: "SENDER_ID",
 appId: "APP_ID",
 measurementId: "G-MEASUREMENT_ID",
};
 
const app = initializeApp(firebaseConfig);
 
// Realtime Databaseを他で使用するためエクスポートする
export const database = getDatabase(app);

App.tsxでRealtime Databaseを接続

次はApp.tsxでRealtime Databaseの情報を取得する実装をします。
こちらは以下の公式ドキュメントを参考にします。
https://firebase.google.com/docs/database/web/read-and-write

まずはuseEffectでonValueをセットアップして、返ってくる値を確認します。

typescript:src/App.tsx
import React, { useEffect, useState } from "react";
import { ref, onValue } from "firebase/database";
import { database } from "./firebase";
 
interface Comment {
 id: string;
 body: string;
}
 
const App: React.FC = () => {
 const [comments, setComments] = useState<Comment[]>([]);
 
 useEffect(() => {
   const commentId = document.location.pathname.split("/").join("");
   console.log(commentId);
   const currentCommentRef = ref(database, `comments/`);
   onValue(currentCommentRef, (snapshot) => {
     const data = snapshot.val();
     console.log(data);
   });
 }, []);
 
 return <h1>Comments For You</h1>;
};
 
export default App;

上記でも説明したように、Realtime DatabaseはJSON形式です。JSONデータベースのどこを見たいのかを指定する必要があります。
ルートのJSON「”/”」だと、データベースの全てを取得してしまいます。
今回は、「/comments/」のところを取ります。実際は「”/comments/posts1”」など、投稿ごとに情報を入れたいです。

Dockerを再度ビルドして、posts/1を見ると、

大丈夫かな、と不安になるが、これはリアルタイムで更新するはずなので、データベースの”/comments/”のところに変化があれば、console.logのcallback関数が実行されるはず!
試してみましょう。

Realtime DatabaseのConsoleでtestを追加してみると、

よし!きたぞ!ひやるがへ!

ページ毎のコメント欄を実装

終盤に入って参りました。最終的な実装をしましょう。

コメントを追加する方法

まずは他のReact部品でコメントを書くフォームを作りましょう。

typescript:src/components/CommentForm.tsx
import React, { FormEventHandler, useRef } from "react";
 
const CommentForm: React.FC<{
 sendComment: (data: { sender: string; body: string }) => void;
}> = ({ sendComment }) => {
 const nameRef = useRef<HTMLInputElement>(null);
 const bodyRef = useRef<HTMLInputElement>(null);
 
 const handleSubmit: FormEventHandler = (event) => {
   event.preventDefault();
   const name = nameRef.current!.value.trim();
   const body = bodyRef.current!.value.trim();
 
   const nameIsValid = name.length > 2 && name.length < 100;
   const bodyIsValid = body.length > 2 && body.length < 500;
 
   if (nameIsValid && bodyIsValid) {
     sendComment({ sender: name, body });
   }
 };
 return (
   <form onSubmit={handleSubmit}>
     <div>
       <label htmlFor="name">名前</label>
       <input type="text" name="name" id="name" ref={nameRef} />
     </div>
     <div>
       <label htmlFor="">コメント</label>
       <input type="text" name="body" id="body" ref={bodyRef} />
     </div>
     <button type="submit">送信</button>
   </form>
 );
};
 
export default CommentForm;

これを親のApp.tsxで使いましょう。

App.tsxではコメントをデータベースに書き込むロジックを追加します。

typescript:src/App.tsx
const sendComment = (formData: { sender: string; body: string }) => {
   push(currentCommentRef, {
     sender: formData.sender,
     body: formData.body,
     sentAt: new Date().getTime(),
   });
 };
<div>
  <CommentForm sendComment={sendComment} />
</div>

Firebase SDKのpush関数を使います。
pushは、”/comments/post1”のところに、新規IDをキーとしてsender, body, sentAtを入れてくれます。
つまり、”/comments/post1”がArrayのようになるのです。

ちなみに、setを使うと、”/comments/post1”にそのままsender, body, sentAtを入れるので、今回は向いていません。

コメントを取得する方法

Firebaseから取得したデータの形と、Reactで使う形が異なります。Reactではinterface CommentからできるArrayのような形が望ましいです。

interface Comment {
 id: string;
 sender: string;
 body: string;
 sentAt: number;
}

しかし、snapshot.val()が返してくれるのはinterface FirebaseCommentSnapshotのような形です。idがキーになってidが入っていないCommentがそれに紐づいているような形です。

interface FirebaseCommentSnapshot {
 [id: string]: Omit<Comment, "id">;
}

この問題を解決するためにはObject.keysを使い、以下のようなコードを書きます。

onValue(currentCommentRef, (snapshot) => {
     const data = snapshot.val() as FirebaseCommentSnapshot;
   if (!data) return; // コメントがなかった場合
     const formattedData: Comment[] = Object.keys(data).map((id) => ({
       id,
       ...data[id],
     }));
     setComments(formattedData);
   });

Comment[]を書く必要はありませんが、これを追加するとコードを書いている段階で正しい形になるまでTypeScriptインタープリターがエラーを表示してくれるので、書きやすいくなります。

全体でこのようにします。

typescript:src/App.tsx
import React, { useEffect, useState } from "react";
import { ref, onValue, push } from "firebase/database";
import { database } from "./firebase";
import CommentForm from "./components/CommentForm";
import "./Index.css";
 
interface Comment {
 id: string;
 sender: string;
 body: string;
 sentAt: number;
}
 
interface FirebaseCommentSnapshot {
 [id: string]: Omit<Comment, "id">;
}
 
const commentId = document.location.pathname.split("/").join("");
const currentCommentRef = ref(database, `comments/${commentId}`);
 
const App: React.FC = () => {
 const [comments, setComments] = useState<Comment[]>([]);
 
 useEffect(() => {
   onValue(currentCommentRef, (snapshot) => {
     const data = snapshot.val() as FirebaseCommentSnapshot;
   if (!data) return; // コメントがなかった場合
     const formattedData: Comment[] = Object.keys(data).map((id) => ({
       id,
       ...data[id],
     }));
     setComments(formattedData);
   });
 }, []);
 
 const sendComment = (formData: { sender: string; body: string }) => {
   push(currentCommentRef, {
     sender: formData.sender,
     body: formData.body,
     sentAt: new Date().getTime(),
   }).catch((error) => console.error(error));
 };
 
 return (
   <>
     <h1>Comments</h1>
     <div className="card">
       <ul className="comment">
         {comments.map((comment) => (
           <li key={comment.id}>
             <p>{comment.sender}</p>
             <p>{comment.body}</p>
             <small>{new Date(comment.sentAt).toLocaleString()}</small>
           </li>
         ))}
       </ul>
     </div>
     <div className="card">
       <CommentForm sendComment={sendComment} />
     </div>
   </>
 );
};
 
export default App;

これで送信したコメントが即時見えてきます!
Reactに渡しているKeyも変わらないので、ページコンテンツが一瞬消えることもありません。

申し訳程度にCSSを入れると、

Realtime Databaseのルールを設定する

現在の設定だと、リクエストを送りさえすれば、好きなだけデータベースの情報を変えられるという好ましくない状態です。悪意ある者が以下のようなリクエストを送れば、データベースは初期化されるのです。

`javascript
fetch("https://realtime-comments-22c1f-default-rtdb.asia-southeast1.firebasedatabase.app/.json", {
   method: "PUT",
   headers: {
     "Content-Type": "application/json",
   },
   body: "{}",
 }
);

これは困るのでルールが必要です。

また、投稿するデータのサイズ(文字列の長さ)には、なんら制限もしていないのでこれもルールでカバーしておきたいです。
この問題を解決するために、Realtime Databaseのルールを設定します。

“comment/postId”でしか書き込みできなくする

Firebase ConsoleのRealtime Databaseダッシュボードからルールのタブをクリックしてルール編集画面を開きましょう。

上記で設定してテストモードの設定が見えます。

試しに.writeをfalseにすると、誰もどこにも書き込めなくなるのです。

Reactアプリで新しいコメントを送信しようとすると、

エラーが出ます。ただ、既存のコメントはまだ見られます。
これがまずルールの基本です。

“comments”のダイレクトリに特化した.readのルール設定

json:rules.json
{
  "rules": {
    "comments": {
      ".read": true,
    },
    ".read": false,
    ".write": false,
  }
}

こうすると、データベースのcomments以外のダイレクトリはデフォルトで読めない状態になります。ただ、”comments/…”のものは全て読めます。

“comments/:postId/:commentId”の書き込みルールの設定

json:rules.json
{
  "rules": {
    "comments": {
      ".read": true,
        "$postId": {
  “$commentId :{
          	  ".write": true,
}
        }
    },
}

$postIdというID変数を定義して、その中身なら、書き込みしていいよ、という設定です。こうすると、”/comments/post1/〇〇”のダイレクトリじゃないと、書き込めないようになっています。

書き込み条件の設定

次は”/comments/:postId/:commentId”の中身の妥当性を評価して、書き込んでいいかどうかのロジックをルールに追加します。そのためには.validateの機能を使います。

json:rules.json
{
  "rules": {
    "comments": {
      ".read": true,
        "$postId": {
  “$commentId”: {
          ".write": true,
          "sender": { ".validate": true },
          "body": { ".validate": true },
          "sentAt": { ".validate": true },
          "$other": { ".validate": false },
}
        },
    },
  },
}

こうすると、sender、 body、 sentAt以外のキーを書き込もうとすると、エラーになります!
続いて、”.validate”の細かい条件を書いてみましょう。

json:rules.json
{
  "rules": {
    "comments": {
      ".read": true,
        "$postId": {
          "$commentId": {
            ".write": true,
            ".validate": "newData.hasChildren(['sender', 'body', 'sentAt'])",
            "sender": { ".validate": "newData.isString() &&
                         newData.val().length > 2 &&
                       newData.val().length < 100" 
                    },
            "body": { ".validate": "newData.isString() &&
                       newData.val().length > 2 &&
                     newData.val().length < 500" 
                    },
            "sentAt": { ".validate": "newData.isNumber() &&
                     newData.val() <= now" 
                    },
          }
        },
    },
  },
}

newDataという変数は、入ってくるJSONのことで、”sender”、”body”などのサブダイレクトリで使うと、自動でJSONのそのサブダイレクトリの値を参照してくれます。
また、.val()で実際のJSONの値を取得し、文字列だったら.lengthなどの値で妥当性評価ができます。
sentAtでは、UNIX時間が現在より小さい数値かチェックしています。

おまけ1:.indexOnについて

.indexOnを指定すると、Reactアプリ側で順番設定をしたら、Reactアプリ側で並べ替えのプロセスを行わずにできます。
今回はsentAtで順番を設定したいので、以下のように”/comments/:postId”のところに以下のルールを追加します。

json:rules.json
{
  "rules": {
    "comments": {
      ".read": true,
        "$postId": {
          ".indexOn": "sentAt",
          "$commentId": {
            ".write": true,
            ".validate": "newData.hasChildren(['sender', 'body', 'sentAt'])",
            "sender": { ".validate": "newData.isString() &&
                         newData.val().length > 2 &&
                       newData.val().length < 100" 
                    },
            "body": { ".validate": "newData.isString() &&
                       newData.val().length > 2 &&
                     newData.val().length < 500" 
                    },
            "sentAt": { ".validate": "newData.isNumber() &&
                     newData.val() <= now" 
                    },
          }
        },
    },
  },
}

おまけ2:onValueの表示件数と順番を設定する方法

コメント数が多い投稿でコメントを全てを取得しようとすると、クライアントのブラウザに負担がかかります。SQLデータベースではLIMIT、ORDER BYなどを使用することで件数制限と並べ替えが簡単にでき、ページネーションなどでブラウザの負担を減らします。
Realtime Databaseにもクエリ条件を設定する機能があります。

今回は取得する件数を制限する方法と、並べ方を指定する方法のみご紹介します。

typescript:src/App.tsx
import React, { useEffect, useState } from "react";
import {
 ref,
 onValue,
 push,
 query,
 orderByChild,
 limitToLast,
} from "firebase/database";
import { database } from "./firebase";
import CommentForm from "./components/CommentForm";
import "./Index.css";
 
const commentId = document.location.pathname.split("/").join("");
const currentCommentRef = ref(database, `comments/${commentId}`);
 
const App: React.FC = () => {
 const [comments, setComments] = useState<Comment[]>([]);
 
 useEffect(() => {
   const limitedComments = query(
     currentCommentRef,
     orderByChild("sentAt"),
     limitToLast(5)
   );
   onValue(limitedComments, (snapshot) => {
     const data = snapshot.val() as FirebaseCommentSnapshot;
     if (!data) return; // コメントがなかった場合
     const formattedData: Comment[] = Object.keys(data).map((id) => ({
       id,
       ...data[id],
     }));
     setComments(formattedData);
   });
 }, []);

limitedCommentsをcurrentCommentsRefの代わりに使うと、最大5件のコメントまで取得できるようになります。

まとめ

これでFirebaseを使ったリアルタイムコメント欄をどのプロジェクトでも追加で導入することができることがわかりました。
Firebaseはとても柔軟なシステムで、複雑な開発を非常にシンプルにしてくれるというのがポイントです。

使い勝手の感想

メリット

・Socket.IOなど、複雑なバックエンドを考えずにリアルタイムの機能をすぐに使える。
・ルールで一定のセキュリティ対策ができ、またデータバリデーションもできる。
・新規サービスを出す時に、バックエンド開発をほぼゼロにできるので、UXベースに開発を進めることができる。

デメリット

・Firebase以外のユーザー認証との組み込み方について検討する必要がある。
参考リンク:https://firebase.google.com/docs/auth/admin/create-custom-tokens
・また、CORS設定ができないので、アクセス制限がかけづらい。

AuthenticationとFunctionsと一緒に使うと、セキュリティを高めて不正アクセスの防止もできます。
ぜひFirebaseについて調べてください!

成果物、ソースコードみたい!という方はこちらへ。

GitHub
https://github.com/tronicboy1/firebase-comments-in-different-site

 

DATUM STUDIOは、Google Cloud サービスを活用し、顧客を分析するための基盤や分析エンジン、実際に施策を実行するためのマーケティング実行基盤までEnd to Endで提供しています。
Google Cloud サービスについてお困りごとがあればDATUM STUDIOまでお気軽にお問い合わせください。

このページをシェアする:



DATUM STUDIOは、クライアントの事業成長と経営課題解決を最適な形でサポートする、データ・ビジネスパートナーです。
データ分析の分野でお客様に最適なソリューションをご提供します。まずはご相談ください。