今更感もありますが、、サーバレスアプリ開発の勉強の為にFirebaseを用いて簡単なチャットアプリを作成しました。
Firebaseについて
Firebaseは、高品質のアプリを迅速に開発できるGoogle のmBaaSです。
今回は主要な機能である下記を使用します。
- Firebase Authentication : アプリへのログイン機能を提供
- Cloud Firestore : 構造化データを保存し、変更時に即座に通知を受け取ることが出来る
- Cloud Storage : クラウドにファイルを保存する
- Firebase Hosting : ホスティングする
- Firebase Cloud Messaging :プッシュ通知を送信する
- Firebase Performance Monitoring : アプリのパフォーマンスデータを収集する
開発環境
- Windows10
- Docker (DockerDesktop for windows(2.2.0.5))
完成イメージ
チャットアプリ開発
1. 環境構築
- codelabsのリポジトリから、ローカルへソースをコピーしてくる
- Firebaseのconsoleよりwebアプリを作成
- [プロジェクトの作成]を行う(プロジェクト名とGoogleアナリティクス有効は任意)
- >(webアプリにFirebaseを追加)を行う(アプリ名は任意、FirebaseHostingの設定はONにする)
- 下記Dockerfileとdocker-compose.ymlをローカルソースのRootへ配置し、
docker-compose up -d --build
コマンドで開発環境コンテナを作成する
Dockerfile
FROM node:10.19-alpine
WORKDIR /app
RUN npm install -g firebase-tools
RUN apk add curl
ENV HOST 0.0.0.0
EXPOSE 5000
EXPOSE 9005
version: '3'
services:
node:
container_name: node
build: ./
tty: true
volumes:
- ./:/app
ports:
- 5000:5000
- 9005:9005
2. FirebaseCLI設定
- 作成したコンテナ内にFirebaseCLIをインストールする
-
docker exec -it node ash
でコンテナ内へ -
npm -g install firebase-tools
よりインストール実施 -
firebase --version
が返ってくればインストール成功
-
- FirebaseCLIへユーザー認証する
-
firebase login
と打つとURL(ttps://accounts.google.com/o/oauth2/auth...)が表示されるので、ブラウザで表示されたURLへ移動 - Googleの認証画面が表示されるので、画面に従い認証を通す
-
cd /app/web-start/
でweb-startディレクトリへ移動 -
firebase use --add
にてアプリとFirebaseを関連付けする(Project IDは前手順のものを選択、aliasは無くてOK) -
firebase serve -o "0.0.0.0" --only hosting
にてローカル上でのアプリ起動 -
hosting: Local server: http://localhost:5000
と表示されれば起動 - ブラウザより http://localhost:5000 を起動し、下記画面が表示されるか確認
-
3. 機能実装
-
Firebaseのconsoleより、下記設定を行う
- 「開発>Authentication」、「sign-in method」からGoogleログインを有効にする
- 「開発>Database」、CloudFirestoreのCreate Databaseを行う([Start in test mode]を選択し、locationは任意選択)
- 「開発>Storage」、「始める」からCloud Storageを作成する(locationは任意選択)
-
web-start/public/index.html
内にFirebaseSDKとSDK構成用のscriptの記載があるか確認 -
web-start/public/scripts/main.js
内のfunctionを下記記載の通りに書き換える
参考)
https://firebase.google.com/docs/reference/js/firebase.auth.Auth
https://firebase.google.com/docs/reference/js/firebase.firestore
https://firebase.google.com/docs/reference/js/firebase.storage
#messagesのデータモデル図
<script src="/__/firebase/X.X.X/firebase-app.js"></script>
<script src="/__/firebase/X.X.X/firebase-auth.js"></script>
<script src="/__/firebase/X.X.X/firebase-storage.js"></script>
<script src="/__/firebase/X.X.X/firebase-messaging.js"></script>
<script src="/__/firebase/X.X.X/firebase-firestore.js"></script>
<script src="/__/firebase/X.X.X/firebase-performance.js"></script>
<script src="/__/firebase/init.js"></script>
<script src="scripts/main.js"></script>
// Signs-in Friendly Chat.
function signIn() {
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider);
}
// Signs-out of Friendly Chat.
function signOut() {
firebase.auth().signOut();
}
// Initiate firebase auth.
function initFirebaseAuth() {
firebase.auth().onAuthStateChanged(authStateObserver);
}
// Returns the signed-in user's profile Pic URL.
function getProfilePicUrl() {
return firebase.auth().currentUser.photoURL || '/image/profile_placeholder.png';
}
// Returns the signed-in user's display name.
function getUserName() {
return firebase.auth().currentUser.displayName;
}
// Returns true if a user is signed-in.
function isUserSignedIn() {
return !!firebase.auth().currentUser;
}
// Saves a new message on the Firebase DB.
function saveMessage(messageText) {
return firebase.firestore().collection('messages').add({
name: getUserName(),
text: messageText,
profilePicUrl: getProfilePicUrl(),
timestamp: firebase.firestore.FieldValue.serverTimestamp()
}).catch(function(error) {
console.error('Error writing new message to database', error);
});
}
// Loads chat messages history and listens for upcoming ones.
function loadMessages() {
// create the query to load the last 12 messages and listen for new ones.
var query = firebase.firestore()
.collection('messages')
.orderBy('timestamp', 'desc')
.limit(12);
// start listening to the query.
query.onSnapshot(function(snapshot) {
snapshot.docChanges().forEach(function(change) {
if (change.type === 'removed') {
deleteMessage(change.doc.id);
} else {
var message = change.doc.data();
displayMessage(change.doc.id, message.timestamp, message.name, message.text, message.profilePicUrl, message.imageUrl);
}
})
})
}
// Saves a new message containing an image in Firebase.
// This first saves the image in Firebase storage.
function saveImageMessage(file) {
// 1 - We add a message with a loading icon that will get updated with the shared image.
firebase.firestore().collection('messages').add({
name: getUserName(),
imageUrl: LOADING_IMAGE_URL,
profilePicUrl: getProfilePicUrl(),
timestamp: firebase.firestore.FieldValue.serverTimestamp()
}).then(function(messageRef) {
// 2 - Upload the image to Cloud Storage.
var filePath = firebase.auth().currentUser.uid + '/' + messageRef.id + '/' + file.name;
return firebase.storage().ref(filePath).put(file).then(function(fileSnapshot) {
// 3 - Generate a public URL for the file.
return fileSnapshot.ref.getDownloadURL().then((url) => {
// 4 - Update the chat message placeholder with the image's URL.
return messageRef.update({
imageUrl: url,
storageUri: fileSnapshot.metadata.fullPath
});
});
});
}).catch(function(error) {
console.error('There was as error uploading a file to Cloud Storage:', error);
});
}
4. 通知の表示
-
web-start/public/manifest.json
を下記記載の通りに書き換える -
web-start/public/firebase-messaging-sw.js
を作成し、下記内容を記載する -
web-start/public/scripts/main.js
のfunctionsaveMessagingDeviceToken
を下記内容で実装する - ここまで出来たら、下記の手順にて通知が表示されることが確認できる
- コンテナ内で
firebase serve -o "0.0.0.0" --only hosting
よりアプリ起動 - http://localhost:5000 へアクセスし、通知の許可を求められるので「許可」にする
- ブラウザからjavascriptのconsoleを開き、
Got FCM device token
の値を控える - FirebaseのconsoleよりCloud Messagingを開きサーバーキーを控える
- 下記記載のcurlコマンドの
YOUR_DEVICE_TOKEN
を上記3、YOUR_SERVER_KEY
を上記4で控えた内容に置換る - 作成したcurlコンテナ内で実行することでブラウザに表示中のチャットアプリから通知が表示される
- コンテナ内で
{
"name": "Friendly Chat",
"short_name": "Friendly Chat",
"start_url": "/index.html",
"display": "standalone",
"orientation": "portrait",
"gcm_sender_id": "103953800507"
}
importScripts('/__/firebase/6.0.4/firebase-app.js');
importScripts('/__/firebase/6.0.4/firebase-messaging.js');
importScripts('/__/firebase/init.js');
firebase.messaging();
// Saves the messaging device token to the datastore.
function saveMessagingDeviceToken() {
firebase.messaging().getToken().then(function(currentToken) {
if (currentToken) {
console.log('Got FCM device token:', currentToken);
// Saving the Device Token to the datastore.
firebase.firestore().collection('fcmTokens').doc(currentToken)
.set({uid: firebase.auth().currentUser.uid});
} else {
// Need to request permissions to show notifications.
requestNotificationsPermissions();
}
}).catch(function(error){
console.error('Unable to get messaging token.', error);
});
}
// Requests permissions to show notifications.
function requestNotificationsPermissions() {
console.log('Requesting notifications permission...');
firebase.messaging().requestPermission().then(function() {
// Notification permission granted.
saveMessagingDeviceToken();
}).catch(function(error) {
console.error('Unable to get permission to notify.', error);
});
}
参考)https://firebase.google.com/docs/reference/js/firebase.messaging
curl -H "Content-Type: application/json" \
-H "Authorization: key=YOUR_SERVER_KEY" \
-d '{
"notification": {
"title": "New chat message!",
"body": "There is a new message in FriendlyChat",
"icon": "/images/profile_placeholder.png",
"click_action": "http://localhost:5000"
},
"to": "YOUR_DEVICE_TOKEN"
}' \
https://fcm.googleapis.com/fcm/send
5. セキュリティルール
-
web-start/firestore.rules
を作成し、下記内容を記載する -
web-start/storage.rules
を作成し、下記内容を記載する -
web-start/firebase.json
へ、下記追記する - コンテナ内で
firebase deploy --only storage,firestore
コマンドにより作成したセキュリティルールをデプロイする
service cloud.firestore {
match /databases/{database}/documents {
// Messages:
// - Anyone can read.
// - Authenticated users can add and edit messages.
// - Validation: Check name is same as auth token and text length below 300 char or that imageUrl is a URL.
// - Deletes are not allowed.
match /messages/{messageId} {
allow read;
allow create, update: if request.auth != null
&& request.resource.data.name == request.auth.token.name
&& (request.resource.data.text is string
&& request.resource.data.text.size() <= 300
|| request.resource.data.imageUrl is string
&& request.resource.data.imageUrl.matches('https?://.*'));
allow delete: if false;
}
// FCM Tokens:
// - Anyone can write their token.
// - Reading list of tokens is not allowed.
match /fcmTokens/{token} {
allow read: if false;
allow write;
}
}
}
参考) https://firebase.google.com/docs/firestore/security/overview
// Returns true if the uploaded file is an image and its size is below the given number of MB.
function isImageBelowMaxSize(maxSizeMB) {
return request.resource.size < maxSizeMB * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
service firebase.storage {
match /b/{bucket}/o {
match /{userId}/{messageId}/{fileName} {
allow write: if request.auth != null && request.auth.uid == userId && isImageBelowMaxSize(5);
allow read;
}
}
}
参考)https://firebase.google.com/docs/storage/security/start
{
//Add this
"firestore": {
"rules": "firestore.rules"
},
//Add this
"storage": {
"rules": "storage.rules"
},
"hosting": {
...
}
}
6. パフォーマンス測定
-
web-start/public/index.html
に下記内容があるか確認 -
web-start/public/scripts/main.js
に下記を追記 - FID計測する場合は、
web-start/public/index.html
へ下記追記 - Firebaseのconsoleからパフォーマンスを確認することが可能となる
<script src="/__/firebase/X.X.X/firebase-performance.js"></script>
<script src="/__/firebase/init.js"></script>
firebase.performance();
<script type="text/javascript">!function(n,e){var t,o,i,c=[],f={passive:!0,capture:!0},r=new Date,a="pointerup",u="pointercancel";function p(n,c){t||(t=c,o=n,i=new Date,w(e),s())}function s(){o>=0&&o<i-r&&(c.forEach(function(n){n(o,t)}),c=[])}function l(t){if(t.cancelable){var o=(t.timeStamp>1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,o){function i(){p(t,o),r()}function c(){r()}function r(){e(a,i,f),e(u,c,f)}n(a,i,f),n(u,c,f)}(o,t):p(o,t)}}function w(n){["click","mousedown","keydown","touchstart","pointerdown"].forEach(function(e){n(e,l,f)})}w(n),self.perfMetrics=self.perfMetrics||{},self.perfMetrics.onFirstInputDelay=function(n){c.push(n),s()}}(addEventListener,removeEventListener);</script>
7. デプロイ
-
/web-start/firebase.json
へdepoyするローカルファイルを指定する -
firebase deploy --except functions
コマンドにてFirebaseHostingへdeployする -
Deploy complete!
との表示されれば完了(URLはHosting URL: https://xxxx
)
{
"firestore": {
"rules": "firestore.rules"
},
"storage": {
"rules": "storage.rules"
},
"hosting": {
"public": "./public"
}
}
最後に
上記はGoogleの提供するcodelabを元に構築しました。
取り敢えずチャットアプリを作成する部分のみを抜粋して記載しております為、より深くFirebaseについて知りたい方は参考元 (Firebase web codelab) をご覧いただければと思います。
また、色々と独学で進めている部分が多い為、上記記事でおかしな点あればやさしくコメント頂けると幸いです。