既存のWebサイトに瞬時更新コメント欄を追加する方法
Firebaseを用いて1日で実装してみた
ちゅらデータのAustin Mayerです。
本記事では、Firebaseを活用して、既存のWebサイトに瞬時更新コメント欄を追加する方法を紹介します。
既存のWebサイトに対して、最新の技術を実装することが難しい場面があります。例えば、WebSocketを使ってチャット機能をホームページに追加したいが、既存のバックエンドはWebSocketに対応するのが難しいような状況が考えられます。
そこで、助けてくれるのは、Google Cloud PlatformのFirebaseです!
目次
- 1 Firebaseとは
- 2 本記事で使われる技術
- 3 擬似既存のwebsiteをセットアップ
- 4 Railsでホームページを作成
- 5 既存のwebsiteにReactを入れる
- 6 Dockerでビルドして、Reactが出ていることを確認
- 7 Firebaseで新規プロジェクトを作る
- 8 Realtime Databaseの有効化
- 9 フロントエンドのReactにfirebaseパッケージをインストールし接続
- 10 ページ毎のコメント欄を実装
- 11 おまけ1:.indexOnについて
- 12 おまけ2:onValueの表示件数と順番を設定する方法
- 13 まとめ
- 14 使い勝手の感想
- 15 成果物、ソースコードみたい!という方はこちらへ。
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までお気軽にお問い合わせください。