|||||||||||||||||||||

なんぶ電子

- 更新: 

Firebase9.5の無料枠でオリジナルのいいねボタン

以前もFirebaseの無料枠でオリジナルのいいねボタンを作成しましたが、当時7.4だったFirebaseのバージョンが9.5と新しくなったので、それに対応するためにコードを改変しました。

新しくなったFirebaseでは「Functions」が「Spark(無料)」プランでは利用できなくなってしまいましたが、今回紹介するいいねボタンはFunctionの利用はありません。

Firebaseの設定

Firebaeプロジェクトの作成

プロジェクトの作成から順を追って説明していきます。

Firebaseにアクセスしてアカウントを作成してログインしたら、プロジェクトを作成します。

名前は任意でつけてください。その次の質問事項であるアナリティクスの設定ついては無効にしました。

authenticationの設定

そのあと「authentication」で認証の設定をします。ページ毎の「いいね」を1人1回に限定するためで、認証によって得られるIDを基準に重複を検知します。ここではGoogleアカウント認証を選択しました。

この登録時、ドメインの設定に自分のサイトのアドレスを登録してください。承認済みドメインにはGoogleアカウント認証を利用できるドメインを指定します。そのため、「localhost」のエントリーに関しては、本番稼働時には後必ず削除してください。

認証の設定

データベースの設定とルール

データベースはfirestoreを利用します。これはNOSQLデータベースで、いいねの数を保存するために使います。

firestoreのデータ概念は次のようになっています。

  • コレクション

    リレーショナルデータベースのテーブルに相当します

  • ドキュメント

    リレーショナルデータベースの行に相当します

  • フィールド

    リレーショナルデータベースの列に相当します

データベースレイアウトは前回から少し変えました。

「コレクション」に対象となる「ページ名」を設定します。「ドキュメント」には「ユーザーID」利用します。「フィールド」には評価を示すratingを持ちます。ratingは1が入っている時に「いいね」の状態、その他の値か値が存在しない場合は「評価無し」とします。

firestoreのレイアウト

データベースの設計ができたら、次にファイアウォールに相当するルールを作ります。「データ」の横に「ルール」というタブがあるのでそこから設定ができます。firestoreのルール構造はリンク先の公式ページにありますが、ここでは次のようにしました。

ルール

service cloud.firestore {
match /databases/{database}/documents {
match /{collection}/{document} {
allow read: if true;
allow write: if request.auth != null && request.auth.uid==document;
}
}
}

最初の2行は固定となっておりfirestoreのデフォルトデータベースに適用されるフィルタとなっています。

次のmatchでは対象となるコレクションとドキュメントを後続で指定しています。

コレクション名/ドキュメント名となっており文字列を入力すると一致するオブジェクトにのみ有効になります。

{}で囲まれたはワイルドカードを意味します。なのでここではすべてのコレクション、ドキュメントが対象になります。中の文字列は一致したオブジェクトを格納する変数となり、後の条件式で利用可能です。ここではコレクション名がcollectionに入り、ドキュメント名がdocumentに入ります。ドキュメント名にはユーザーのIDを利用しているのでこのdocument変数を使って、ログイン済みのユーザーIDと照合します。

ここでは使用していませんが{document=**}等となっているものは、フィールド内のサブコレクションを対象にするための「再帰ワイルドカード」構文です。

:ifで条件式を作ります。直前の権限を:if以下の条件が成立する時のみ与えることができます。無条件に許可するには:if true; 拒否するには:if falseとします。

request.authで認証を受けたユーザーをに書き込みを限定するとともに、document名とrequest.auth.uidと比較して、自身のIDのドキュメントの場合だけ書き込みを許可しています。

アプリの登録

プロジェクトの設定

ここまで設定したFirebaseをアプリとして公開します。プロジェクトのトップメニュー左側の「プロジェクトの設定」より、アプリの追加を選択します。

ここでは、アプリの種類はウェブアプリ</>を選択します。

アプリのニックネームは内部的に使うだけなのでわかりやすい名前をつけてください。

アプリは自サイトにある前提なので、Hostingの設定にはチェックを入れません。

アプリが作成されると「プロジェクトの設定」画面の「マイアプリ」にapiKeyなどの情報が表示されるようになります。

アプリの追加

コード

公式の導入ガイドを参考にコーディングしました。

以前はそのままCDNで使えましたが、バージョン9系はSDKで開発した後 モジュールバンドラ使ってWebアプリを作成することが(強く)推奨されています。それにより不要なコードを除去してアプリを軽量化することができます。

しかし、ここではバンドルせずに記述する方法を紹介します。SDK開発が推奨されコードがモジュール化されたことに伴って、HTMLにコードを書く時もモジュール(type="module")で書く必要があります。モジュールでは通常のJavaScriptとスコープが違うため、HTMLのonclick属性の記述で関数を起動させることができませんので注意してください。

sample.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>いいねボタンサンプル</title>
</head>
<body>
<h3>サンプル</h3>
<div>
<p>よろしかったら評価お願いいたします</p>
<p>Googleアカウントでのサインインが必要です。ポップアップを許可してください。</p>
<p><button class="ene" id="btnene" style="background-color:#ffffff;"><span id="spancount">0</span> いいねぇ</button></p>
<p style="text-align: right;"><a href="javascript:void(0);" id="signout-target">サインアウト</a></p>
</div>

<script type="module">
// インポート
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.5.0/firebase-app.js";
import { getAuth, signInWithPopup, GoogleAuthProvider, signOut, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/9.5.0/firebase-auth.js";
import { getFirestore, collection, getDocs, setDoc, doc, query, where } from "https://www.gstatic.com/firebasejs/9.5.0/firebase-firestore.js";

let strUid=null; //ユーザーID保存
let buttonColor = '#fff';

//Firebase configuration
const firebaseConfig = {
マイアプリに表示されるfirebaseConfigをコピー
};

//Firebaseイニシャル
const app = initializeApp(firebaseConfig);

//Googleプロバイダオブジェクトのインスタンスを作成
const provider = new GoogleAuthProvider();
const auth = getAuth();

//Firestore
const db = getFirestore(app);

//ページのID(URLからドメインを抜いた文字列)
// /がfirestore上で区切りとして作用してしまうので|に変換する
let strPageId = location.pathname.replace(/\//g,'|');

//いいねをもらっているIDのリスト
let eneIds = [];

//いいねボタン
let elButton = document.getElementById('btnene');
//いいねの数の表示域
let elCount = document.getElementById('spancount');

//サインインの処理
const signInFunc = async ()=> {
await signInWithPopup(auth, provider)
.then((result) => {
strUid = result.user.uid;
//console.log(result);
}).catch((error) => {
strUid = null;
cosole.log(error);
});
}
//サインアウトの処理
const signOutFunc = ()=> {
signOut(auth).then(() => {
console.log('signeout');
}).catch((error) => {
console.log('signeout error');
});
}
//サインインの状態が変わった時の挙動
onAuthStateChanged(auth, (user) => {
if (user) {
//サインインした時(またはサインインの状態でアクセス)
console.log(user.uid+" sign in");
strUid = user.uid;//サインインの状態でアクセスした場合はここでuidがセットされます
if (0 <= eneIds.indexOf(strUid)) {
elButton.style.background='#fcc';
} else {
elButton.style.background='#fff';
}
} else {
//サインアウトした時(またはサインアウトの状態でのアクセス)
console.log("sign out");
strUid = null;
elButton.style.background='#fff';
}
});

//いいねをFirestoreに反映
const updateEne = async(intRating) => {
await setDoc( doc(db, strPageId, strUid), {
rating: intRating
}, { merge: true });
}

const checkLoginAndEne = async () => {
if (strUid) {
//ログイン済み
if (0 <= eneIds.indexOf(strUid)) {
//いいねからOFF
updateEne(0);
elButton.style.background='#fff';
eneIds.splice(eneIds.indexOf(strUid),1);
} else {
//いいねに
updateEne(1);
elButton.style.background='#fcc';
eneIds.push(strUid);
}
} else {
//ログイン
await signInFunc();
console.log(strUid);
if (strUid==null) {
//ログイン失敗
elButton.style.background='#fff';
console.log('login failed');
} else {
if (0 <= eneIds.indexOf(strUid)) {
//すでにいいね
elButton.style.background='#fcc';
} else {
//いいねに
updateEne(1);
elButton.style.background='#fcc';
eneIds.push(strUid);
}
}
}

elCount.innerHTML=String(eneIds.length);
}

const getEneIds = async () => {
//いいねを取得しているIDを取得
const q = query(collection(db, strPageId), where("rating", "==", 1));
const querySnapshot = await getDocs(q);
eneIds = [];
querySnapshot.forEach((doc) => {
console.log(doc.id, " => ", doc.data());
eneIds.push(doc.id);
});

//ログインの状態と評価の確認
if (strUid) {
if (0 <= eneIds.indexOf(strUid)) {
elButton.style.background='#fcc';
} else {
elButton.style.background='#fff';
}
} else {
elButton.style.background='#fff';
}

elCount.innerHTML=String(eneIds.length);
}

//Ver9系ではモジュールで利用する必要があるので、ここでイベントを設定します
elButton.addEventListener('click', checkLoginAndEne);
document.getElementById('signout-target').addEventListener('click', signOutFunc);

//いいねの数を取得
getEneIds();

</script>
</body>
</html>

apiKeyを公開しているけれど大丈夫なのか

shiodaifuku.ioでは、アプリのキーを公開しても問題ないと判断していたり(参考になりました。ありがとうございました。)、Firebaseでユーザーを認証しているSKY FMのサイトでも、ページソースから利用しているapiKeyを読むことができますが、本当にJavaScript内のコード内に含めて(公開して)大丈夫なのでしょうか。

機能別に今回のアプリを検証してみたいと思います。

  • Googleアカウント認証

    Googleアカウント認証については、先に記述した通り「承認済みドメイン」のリストにより認証リクエストの発生元を制限できているので問題ないと判断します。ただしlocalhostを含めないようにしてください。この認証の仕組みは過去の記事「GoogleのOAuth2.0でセキュアな認証」も参考にしていただければと思います。

  • firestore

    firestoreの読み出しに関しては特性上何も制限をしていません。Webにページを公開するのと同様すべて見られてもいいという前提でデータを格納します。なので、認証時に利用者のメールアドレス等も取得できますがそれらはデータベースには格納してはいません。ここでデータベースに保存するユーザーIDについてはアプリ毎の値だということなので、外部に見られて問題ないと判断しています。

    書き込みに関しては、認証を受けたユーザーが自分のIDと同じ名前のドキュメントへの書き込みを許しています。条件に一致するデータを外から書き込まれることはあるかもしれませんが、その際はユーザーIDがわかりますので、コンソールの「Authentication」メニューからユーザーを無効にできます。

稼働サンプル

よろしかったら評価お願いいたします

Googleアカウントでのサインインが必要です。ポップアップを許可してください。

サインアウト

アプリが取得する個人情報の取り扱い等、当ブログのプライバシーポリシーに関してはこちらのページをご参照ください。

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告