はじめに
Firebaseを用いたサービスを作る演習として、導入から昔ながらの掲示板(BBS)を実装するまでをちゃちゃっと行ってみます。
最短の実装のために色々と端折っているのでご容赦ください。
BBSの仕様
- 名前欄と本文と削除キーを入力、投稿ボタンを押すことで投稿
- 今まで投稿された内容のうち直近10件がリスト表示される
- それぞれの投稿の削除ボタンから投稿の削除ができる。その際削除キーが尋ねられ、本来のキーと一致する場合にのみ削除が行われる
ユーザー認証とかもない古き良き電子掲示板ですね。
Node.jsのインストール
Firebase CLI の使用にあたって必須なので公式サイトからダウンロードしてきてインストールします。
Firebase CLI のインストール
Node.jsがインストールできたら、今度はFirebase CLIをインストールします。コマンドラインでnpm install -g firebase-tools
と入力:
$ npm install -g firebase-tools
無事インストールできたらfirebase
コマンドが有効になっている筈です:
$ firebase
# (何か色々メッセージが出てくる)
Firebaseプロジェクトを作る
基本的な部分は Firebaseの始め方 - Qiita の Firebaseを初めてみる の項と同じです。
-
https://console.firebase.google.com/ にアクセス、Googleアカウント(無かったら作る)でログイン。
-
プロジェクトを追加
→適当にプロジェクト名を入力。 -
firebase login
コマンド→ブラウザでログイン画面が開くので先のGoogleアカウントを選択。許可ボタンを押してFirebase CLIでのログイン完了 -
適当にプロジェクト用のディレクトリを新規作成し、そこに移動。
firebase init
コマンドでFirebaseプロジェクトの初期化を開始:
$ cd your_firebase_project_dir
$ firebase init
? Are you ready to proceed?
# => Yes
? Which Firebase CLI features do you want to setup for this folder?
# => 今回は `Database`, `Functions`, `Hosting` を選択
? Select a default Firebase project for this directory:
# => 先に作成したFirebaseプロジェクトを選択
? What file should be used for Database Rules?
# => そのままエンター(デフォルト名を使用)
? What language would you like to use to write Cloud Functions?
# => JavaScriptを選択
? Do you want to use ESLint to catch probable bugs and enforce style?
# => No
? Do you want to install dependencies with npm now?
# => Yes
? What do you want to use as your public directory?
? Configure as a single-page app (rewrite all urls to /index.html)?
# => どちらもそのままエンター(デフォルト)
...
+ Firebase initialization complete!
これでプロジェクトの初期化は完了です。
上のとおりに初期化を行った後では、プロジェクトのルートディレクトリ下には
- ファイル
database.rules.json
,firebase.json
- Hosting用の
public
ディレクトリ - Cloud Functions用の
functions
ディレクトリ
が生成されている筈です。
コードを書く
Hosting側
publicディレクトリ下に配置するファイルの内訳はindex.html
, main.js
, main.css
の3つとします。
index.html
は次の通り:
<html>
<head>
<title>Firebase Simple BBS</title>
<script defer src="/__/firebase/4.9.1/firebase-app.js"></script>
<script defer src="/__/firebase/4.9.1/firebase-database.js"></script>
<script defer src="/__/firebase/init.js"></script>
<script src="./main.js"></script>
<link rel="stylesheet" type="text/css" href="./main.css"></link>
</head>
<body>
<div id="reply-form">
<div class="l">Name:</div> <input type="text" id="reply-name">
<div class="l">Body:</div> <textarea id="reply-body"></textarea>
<div class="l">Key:</div> <input type="text" id="reply-key">
<div class="l"></div> <button id="reply-submit" onclick="postReply()">Post</button>
</div>
<div id="replies"></div>
</body>
</html>
public/main.js
はこう:
const main = ()=>
firebase.database().ref('/simplebbs/posts').limitToLast(10).on('value', snapshot=>{
const posts = snapshot.exists() ? snapshot.val() : {}
let html = ''
for(const [id, {name, content, date}] of Object.entries(posts).reverse())
html += makeReply(id, name, content, date)
document.querySelector('#replies').innerHTML = html
})
const makeReply = (id, name, content, date) => `<div class="reply">
<div class="head">Name: ${name} <span class="date">${date}</span></div>
<div class="content">${content}</div>
<button class="delete" onclick="deleteReply('${id}')">delete</button> </div>`
const postReply = ()=> post('/api/post', {
name: document.querySelector('#reply-name').value,
content: document.querySelector('#reply-body').value,
key: document.querySelector('#reply-key').value,
}).then(e=>{ document.querySelector('#reply-body').value='' })
const deleteReply = id => post('/api/delete', {id, key: prompt('key?') || ''})
const post = (path, jsonData) => fetch(path, {
method: 'POST', body: JSON.stringify(jsonData),
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'},
})
document.addEventListener('DOMContentLoaded', main)
public/main.css
は適当に
Cloud Functions側
Cloud Functions 側のファイルfunctions/index.js
を下記の内容に書き換えます:
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)
const db = admin.database()
const rep = s=> s.replace(/</g, '<').replace(/>/g, '>').replace(/\n/g,'<br>')
exports.post = functions.https.onRequest((request, response)=>{
const {name, content, key} = request.body
const date = new Date().toLocaleDateString()
db.ref('/simplebbs/posts').push({name:rep(name), content:rep(content), date})
.then(e=> db.ref(`/simplebbs/keys/${e.key}`).set(key))
.then(e=> response.status(200).end())
})
exports.delete = functions.https.onRequest((request, response)=>{
const {id, key} = request.body
db.ref(`/simplebbs/keys/${id}`).once('value').then(sKey=>{
if(!sKey.exists() || sKey.val() !== key)
return response.status(400).send('invalid id or incorrect key').end()
db.ref(`/simplebbs/posts/${id}`).remove()
.then(e=> sKey.ref.remove())
.then(e=> response.status(200).end())
})
})
設定ファイル
firebase.json
にFunctionsのAPIをHosting上のドメインで呼ぶためにrewrites
の項目を書き加えます:
{
"database": {
"rules": "database.rules.json"
},
"hosting": {
"public": "public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{"source": "/api/post", "function": "post"},
{"source": "/api/delete", "function": "delete"}
]
}
}
また Realtime Database のセキュリティルールを設定するためにdatabase.rules.json
を編集:
{
"rules": {
".read": "false",
".write": "false",
"simplebbs":{
"posts":{
".read": "true",
},
},
}
}
デプロイ
$ firebase deploy
完了するまでしばらく待つ
動作確認
デプロイが完了した時に表示されるHosting URL (https://~~.firebaseapp.com
)にアクセス。正しく動作していることを確認します。
これでFirebaseを用いたBBSが設置できました。やったね!
しかもRealtime Databaseを利用しているため無駄にリアルタイムで更新が反映されます。
解説
ページ側 (Hosting側)
-
ページ読み込み時に実行される
main()
関数ではFirebase の Realtime Database からパス/simplebbs/posts
に配置されてある各投稿内容の内、直近10件を取得してページ内に描画しています。 -
投稿(
Post
)ボタンを押すとpostReply()
関数が実行され、その中でCloud Functions側のAPIとして用意したpost
関数をPOSTメソッドのHTTP通信を通して呼んでいます。送信するデータとして、投稿フォームの名前欄・本文・削除キーそれぞれを与えています。 -
同様にそれぞれの投稿の削除(
delete
)ボタンを押すとページ側のdeleteReply()
関数を通してFunctions側のdelete
関数が呼ばれます。
サーバー側 (Cloud Functions側)
-
Cloud Functions側で用意しているAPIは
post
,delete
の2つです。 -
post
関数 : POSTメソッドで投稿内容のデータを受け取り、それをRealtime Databaseに保存。データベース中のパス/simplebbs/posts/{id}
と、/simplebbs/keys/{id}
とで別々の箇所に分割してデータを保存しています。データベースのセキュリティルールより、ユーザー側から(ページ側から)中身を覗けるのは前者の内容のみとなっています。 -
delete
関数 : 同様にPOSTメソッドでデータを受け取り、与えられたIDの投稿の削除キーが入力値と一致する場合にデータベースからその投稿を削除する処理を行っています。
database.rules.json
で書いたデータベースのセキュリティルールにより、ページ側から内容を読み取れるのは一部だけ、また内容の書き込みは全部禁止としています。
サーバー側からは読み書きが行えるので、データの追加やプライベートなデータの扱いはサーバー側に任せています。
注意点
注意点として、上の内容ではコードの簡略化のために尽くエラー処理を省いています。1
所感
- 一昔前のCGIでやっていたようなこともFirebaseなら簡単に実現できてしまいますね。
サーバーの存在が抽象化されている分、下手するとCGIよりも敷居が低いかも - はじめRealtime Databaseばかり取り沙汰されるFirebaseでユーザー側から隠蔽したい処理を含んだサービスはどう記述したらいいものかと疑問に思っていましたが、Cloud Functions for Firebaseを利用すればよかったんですね。素直な形でサーバー側の処理を書けるのはとても楽。
- 過疎・内輪用サービスなら無料プランの範囲内で余裕で遊べる感じです。皆もウェブサービスやコミュニティつくろう
代案あれこれ
- Cloud Functionsを使用せずRealtime Databaseだけで何とかする
- Realtime Databaseのセキュリティルールを駆使すれば、今回のようなプライベートなデータの扱い・削除キーによる投稿の削除といったものもCloud Functions無しで実現できると思います。
- ただサーバー側での投稿内容のサニタイズができないというデメリットも。
- Firebase Storage を使う
- 画像の投稿機能を追加したい時にでも
- クライアントからのDatabase呼び出しは用いずCloud FunctionsのみでSSRする
- より古風なCGIに近づきますね
- ただAPI呼び出し制限の面からあまり経済的ではなさそう
参考
関連
-
例えばCloud Functions側のAPIでは、本来ならばリクエストがPOSTメソッドであることを確認するといった処理が必要です。他にもデータベースの変更の際にエラーが生じた場合への対応や、投稿ボタンを押す際にユーザーが間違ってボタンを連打してしまわないようにする方策等々キチンとしようとするとキリがありませんが、最低限の機能を示す雛形としてはこれで十分でしょう。 ↩