前回は『【Webアプリ作成】Firebaseを使ってリアルタイムでデータを可視化する要点【最短ルート】』という記事でFirebaseを使ったWebアプリの作り方をざっと書いてみましたが、勢いに乗って2個目のWebアプリを開発しようとすると、1個目とは少し違った流れをくまないといけない場合があることに気付きました。
また、2個目というと1個目では使わなかった機能に手を出そうとしてそこでも別のエラーが・・・ということは往々にしてあるわけで、今回はデータベースにはRealtime DatabaseではなくCloud Firestoreを選び、Cloud Functionsの言語はJavaScriptではなくTypeScriptを選び、また比較的長期的な開発を見込んで本番環境と開発環境を分けて作ることにしたため、備忘録がてら作業の流れと注意点をメモすることにしました。
それでは本題に入ります。
前回と同様にまずは新規プロジェクトの作成を・・・となりそうですが、ここで踏みとどまって、まずはローカルにあるホスティング用のFirebaseフォルダを確認してみます。
初めてアプリを作ったときに(私のように)特に深く考えていなければ、Firebaseフォルダ(=『firebase init』コマンドを実行したフォルダ)は固有アプリ用のパスというよりはFirebase共通のパスになっているかもしれません。その場合、複数のアプリを並行して管理することが難しいため、フォルダ構造を少し書き換えます。
before | C:\USERS\{USERNAME}┗Firebase
┗1個目のアプリファイル群 |
---|---|
after | C:\USERS\{USERNAME}┗Firebase
┗{1st_app_name} ┗1個目のアプリファイル群 ┗{2nd_app_name} ┗2個目のアプリファイル群 |
フォルダの整理をした後は、1回目と同じ作業に・・・と言いたいところですが、今回はCloud FirestoreとStorageを利用するので、アプリ追加時にエラーが起きないように、前回にはない作業①~②を先に実行します。
作業①:左タブの『Database』を押してCloud Firestoreのページを開き、『データベースの作成』ボタンを押して作業に入ります。データベースの作成は初期設定のままでもよいですが、Cloud Firestoreのロケーションは利用地に近い方がよいので、初期設定の『nam5(us-central)』から東京である『asia-northeast1』に変更します。
作業②:左タブの『Storage』を押して『始める』ボタンを押して、初期設定のままで『完了』ボタンを押すと、デフォルトバケットが設定されて完了します。
ということで作業①~②が完了したら、1回目のときの作業をなぞるように進んでいきます。
src/index.ts:1:1 - error TS6133: 'functions' is declared but its value is never read.
というエラーが発生します。初期設定のままなのになぜ・・・と思ってしまうところですが、Cloud Functionsを書いていない状態(作ったばかりだから当然)なのに読み込もうとするのが初期設定になっているらしく、早速チェック機能でエラーを吐いているようです。取り急ぎは当該ファイルの1行目にある
import * as functions from 'firebase-functions';
をコメントアウトしてからデプロイすると、そのままスムーズに進み、
https://{YOUR_PROJECT_ID}.firebaseapp.com
https://{YOUR_PROJECT_ID}.web.app
どちらのURLからでもデフォルトページが確認できるようになります。これにて『2.初回に倣ってデプロイ編』は完了です。懐かしのデフォルトページが閲覧できました。
懐かしのデフォルトページ
しかし残念ながら、今回は明示的にエラーが出たので気付いてしまいました。Cloud FunctionsでTypeScriptを選んだときに宛がわれる静的コード解析ツール(リントツール)はTSLintなのですが、TSLintは2019年中に非推奨になることが決まっている古いツールなのですよね。まだまだ現役とは言っても、何とも言い難い気持ちに・・・
天下のGoogle謹製のFirebaseなのに何故対応が遅いのか・・・と嘆いてもしょうがないので、ESLintを個別に手動インストールすることにしました。
なお、TSLintが入っている状態からの作業は面倒なので、TSLintなしで再インストールした段階からの再開ということで次章に移ります。
ということで、FirebaseにESLintを手動インストールしてみます。
(Firebase、楽々環境構築のはずだったのに細かいことを気にしようとすると、やっぱり手間がかかるようです。むむむ・・・という感じです。。)
1 |
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier eslint-config-prettier eslint-plugin-prettier |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
{ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "prettier/@typescript-eslint" ], "plugins": [ "@typescript-eslint" ], "env": { "es6": true, "browser": true, "node": true, "jquery": true }, "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2019, "sourceType": "module", "project": "./tsconfig.json" }, "rules": { } } |
1 2 3 4 |
"scripts":{ "lint": "eslint src/index.ts", "lint:fix": "eslint --fix src/index.ts" } |
1 2 3 4 5 6 |
"functions": { "predeploy": [ "npm --prefix \"$RESOURCE_DIR\" run lint", "npm --prefix \"$RESOURCE_DIR\" run build" ] } |
これにてESLintの設定が完了です。
ESLintは個別設定をしだすとキリがないというか沼に嵌ってしまいそうなのでrecommended頼みの姿勢を全面に押し出したつもりですが、「そうはいってもprettierは必須だよなあ」とか「envやparseOptionsはそれなりに設定しないとなあ」とか悩み始めて難しさの一端を感じたところで最後のステップに移ります。
さて、これだけでも十分に2個目のアプリを作ることはできるのですが、比較的長期的な開発を見込んで本番環境と開発環境を分けて作ることにします。環境構築のイメージはこちらの通りです。
要は、「Firebaseのプロジェクトを2つ作って片方を本番環境、もう片方を開発環境として用意し、『.firebaserc』ファイル上で両方のプロジェクトを定義すると、コマンドラインでどちらの環境にデプロイするかを切り替えることができる」というのがFirebaseの開発環境の考え方です。
具体的な作業としてはシンプルに、
1 2 3 4 5 6 |
{ "projects": { "production": "{YOUR_PRODUCTION_PROJECT_ID}", "development": "{YOUR_DEVELOPMENT_PROJECT_ID}" } } |
もしステージング環境も作りたい場合は、ステージング環境用のプロジェクトを作り、"staging"キーに対してプロジェクトIDをセットすれば対応可能です。
開発環境の構築は、ESLintの対応と比べると作業が段違いにシンプルで良かったです。
準備ができたので、いざ開発に入ります。
今回は初回と違ってCloud Firestoreを使うので、Cloud Firestoreの準備の流れを確認しつつ、データ処理の基本をCRUDごとに確認します。
1 2 3 4 5 6 7 8 9 10 |
let db = firebase.firestore(); //コレクションにIDランダムにてドキュメント追加 db.collection('{COLLECTION_ID}').add({data_object}); //コレクションにIDランダムにてドキュメント追加 db.collection('{COLLECTION_ID}').doc().set({data_object}); //コレクションへID指定にてドキュメント追加(存在する場合は上書き) db.collection('{COLLECTION_ID}').doc('{DOCUMENT_ID}').set({data_object}); |
シンプルな1本のツリーだったRealtime Databaseと比較すると、コレクションの単位がツリーの単位という感じで複数のツリーが存在できるイメージで、コレクション配下にぶら下げる複数の枝としてドキュメントが存在するイメージです。
Realtime Databaseと同じでNoSQL形式(多重連想配列チック)なので、ドキュメントには常に固有のIDと値がセットされます。連想配列としてIDを意識したい場合はIDを明示的に設定すると良いですし、そうでなくただの配列として扱いたい場合はIDを明示せずランダムに任せてドキュメント追加すればよいと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
let db = firebase.firestore(); //コレクション内の1つのドキュメントを1度だけ取得する db.collection('{COLLECTION_ID}').doc('{DOCUMENT_ID}').get().then(function(document){ if (document.exists){ //ドキュメントがある場合 let data = document.data(); } else { //ドキュメントがない場合 }); }); //コレクション内の1つのドキュメントと同期してリアルタイムアップデートをかける db.collection('{COLLECTION_ID}').doc('{DOCUMENT_ID}').onSnapshot(function(document){ if (document.exists){ //ドキュメントがある場合 let data = document.data(); } else { //ドキュメントがない場合 }); }); //コレクション内の全ドキュメントを1度だけ取得する db.collection('{COLLECTION_ID}').get().then(function(snapshot){ snapshot.forEach(function(document){ //ドキュメントごとの処理 let data = document.data(); }); }); //コレクション内の全ドキュメントと同期してリアルタイムアップデートをかける db.collection('{COLLECTION_ID}').onSnapshot(function(snapshot){ snapshot.forEach(function(document){ //ドキュメントごとの処理 let data = document.data(); }); }); |
基本の型としては、
①1つのドキュメントを取得するか/全てのドキュメントを(コレクションごと)取得するか
②1度だけ取得するか/リアルタイムアップデート(リッスン)するか
の2軸で変わってくるイメージです。特に②については、『.get().then』,『.onSnapchat』と関数レベルで変わってくるので、ここは明確に使い分けられるようにしたいところです。コレクションとドキュメントの階層構造を理解すれば、Realtime Databaseとさほど変わらない印象です。なお、他にもリッスンの場合には変更種別(added/modified/removed)ごとの取得などのテクニカルな実装もありますが、ここでは割愛します。
1 2 3 4 5 6 7 8 9 10 |
let db = firebase.firestore(); //コレクションへID指定にてドキュメントをフィールド指定で上書き db.collection('{COLLECTION_ID}').doc('{DOCUMENT_ID}').update({ {FIELD_ID1}:{FIELD_VALUE1}, {FIELD_ID2}:{FIELD_VALUE2}, ... }); //コレクションへID指定にてドキュメントを全上書き db.collection('{COLLECTION_ID}').doc('{DOCUMENT_ID}').set({data_object}); |
シンプルに全上書きする場合は『set』でよいですが、キー単位で更新したい場合は『update』を使います。
1 2 3 4 5 6 7 8 9 10 11 12 |
let db = firebase.firestore(); //コレクションの1つのドキュメントの削除 db.collection('{COLLECTION_ID}').doc('{DOCUMENT_ID}').delete(); //コレクション配下の全ドキュメント削除 db.collection('{COLLECTION_ID}').onSnapshot(function(snapshot){ snapshot.forEach(document => { let id = document.id; db.collection('{COLLECTION_ID}').doc(id).delete(); }); }); |
ウェブクライアントからは、コレクションの削除や複数ドキュメントの一括削除はできず、ID指定で1個ずつ消すだけとなっています。クライアント側からの処理はセキュリティリスクが高いので、まあしょうがないところだと思います。どうしても無理矢理やらないといけない場合は、ドキュメントをループして1個1個IDを取得して削除処理を繰り返す方法を覚えておきます。
ということでCRUDごとに主要な処理を確認してみました。Cloud Firestoreは他にもQueryやOrderByやSubCollectionなどの機能があったり言語ごとに使える機能(や表記)が変わっていたりなどするので、詳しくは公式のドキュメントを参照しましょう。
ということで、このあたりのデータ基本操作を理解しておけば、Cloud Firestoreを使った最低限のウェブアプリを実装できると思います。
ただ、最低限と言うのは、これだけだとセキュアなデータ処理には程遠い状態だからです。
クライアントサイドで動くJavaScript頼みのデータ処理は対策なしだとセキュリティホールだらけになるので、『firestore.rules』の適切な設定が必要です。また、重たいデータ処理や多彩なトリガーを実装する場合はサーバーサイドで動く『Cloud Functions』が適切かもかもしれません。また、実際にデータ構造を考えるときに、NoSQLならではの処理しやすい設計を考えるのが大きな課題です。あと、デザインを整えるうえでは適切なフレームワークの導入と選定が求められるケースが多いでしょう。
さくっとアプリが作れそうなFirebaseでも、ちゃんと作ろうとすると色々と考えないといけないというのは同じなのだなという印象でした。