與 Firebase 再續前緣

與 Firebase 的前緣

以前曾經待過一家超愛使用 Firebase 作為後端的接案公司工作,當時是我第一次接觸到 Firebase, Firebase 底下有許多產品,如:具有運算功能的 functions、資料庫的 RealTime Database、檔案存儲的 Cloud Storage…。 印象深刻的就是 RealTime Database 它是一種 NoSQL 資料庫,無須事先定義 DB schema, 彈性是很高,但是應用在當時儲存需求為結構嚴謹的資料上,顯然就有許多問題, 像是在合作開發下的應用,每個人的程式都對資料庫的文件補刀,導致同一類型的資料結構不一致。

以為使用 Firebase 快速方便能夠節省時間成本,結果反而花了更多時間在處理程式的例外錯誤, 當時算是被迫使用錯誤的解決方案,雖然不是 Firebase 的錯,但從此讓我對 Firebase 有著負面印象,畢竟是不好的回憶阿。 但必須說在當時前端能夠即時響應資料變化的功能真的很潮。

與 Firebase 的再續前緣

最近工作上的需求讓我想到 Firebase 的用武之地, 整個網站是靜態的,只有少數幾個特例是要能夠即時反映資料更新於網頁上,資料都是單例,不會因一般使用者操作有多例資料的產生, 僅會隨一般使用者的操作而有資料的更新。

舉例來說,像是一個 counter 計數器,每個使用者都可以對這個計數器進行 +1 的操作,大家看到的都是同一組數字。 這樣的需求需要後端的計算及儲存功能,但如果要為了這樣簡單的需求開一個網站框架的專案,架設一個後端伺服器或將應用跑在現有 K8S 叢集上,實在是太浪費資源了。
此時 Firebase 就派上用場了:

  • Functions:使用它的計算功能可以讓我們建立一個 API 來執行 +1 的操作,提供給前端調用。
  • Realtime Database:使用它的資料庫功能來儲存計數器的數字,同時基於它的 Realtime 功能,前端可以在不重整網頁的情況下即時看到數字的變化。

已經有好幾年沒有接觸 Firebase,隨著它的改版有些使用跟當時不太一樣,所以記錄這次的使用。

建立 Firebase 專案

# 安裝 Firebase CLI
npm install -g firebase-tools

# 建立專案資料夾
mkdir counter && cd $_

# 初始化 Firebase 專案
# 第一次使用會需要登入 Google 帳號,登入後會提示選擇專案現有或建立新專案
# 選擇要使用的功能:Functions 及 Realtime Database
firebase init 

# 為了在本地端測試,需要安裝 Firebase Emulator
# 選擇要安裝的模擬器:Functions 及 Realtime Database
# 如果沒有安裝 Java,會提示安裝,安裝完後再執行一次 firebase init emulators
firebase init emulators 

檔案結構

初始化完專案後,得到的檔案結構如下:

├── .firebaserc
├── .gitignore 
├── database.rules.json
├── firebase.json
└── functions 
    ├── .gitignore
    ├── index.js
    └── package.json
  • firebase.json:我們用了兩個服務,所以在當中會看到 functionsdatabase 的設定,這些設定會在模擬器或 firebase deploy 部署被使用。
  • .firebaserc: Firebase 專案 ID,這個 ID 會在 firebase deploy 時被使用。
  • database.rules.json:資料庫的規則,預設為禁止讀取及寫入資料。
  • functions:放置 Firebase Functions 的目錄,裡面的 index.js,這是主要程式碼,在搭建 functions 時也可以安裝我們想要使用的 JS 套件。

資料庫規則設定

database.rules.json 檔案中,我們加入一條 number.read 的規則,讓 number 欄位的資料可以被公開讀取,但無法公開被寫入:

{
  "rules": {
    ".read": false,
    ".write": false,
    "number": {
      ".read": true
    }
  }
}

functions 開發

進入 functions 目錄後安裝 expresscors 兩個套件:

cd functions
pnpm install express cors

然後在 package.json 當中加入 "type": "module" 以支援 ES Module 寫法。

編寫 index.js

import functions from "firebase-functions";
import admin from "firebase-admin";
import cors from "cors";
import express from "express";

const app = express();
admin.initializeApp();

app.use(cors({ origin: true }));
app.post('/number', async (req, res) => {
  const ref = admin.database().ref("number");
  const { snapshot } = await ref.transaction(value => (value || 0) + 1);
  res.json({ data: snapshot });
});

export const counter = functions.https.onRequest(app);
  • 我們已經在 database.rules.json 設定 number 欄位為能夠公開讀取,不能公開被寫入,我們要讓寫入的情況在此 API 發生。
  • 使用 admin 的身份來讀取 number 的值,並將值 +1 後寫回資料庫。
  • 藉由 export const counter 會產生出 //.../counter/number 的 API 位置,其中 counter 可以當作是我們的 namespace。

本地測試

回到專案根目錄,啟動模擬器:

firebase emulators:start

留意模擬器的提示訊息會看到 database 的 UI 網址 http://127.0.0.1:4000/database, 可以在該頁面觀察資料庫的狀態,如有資料變更都會即時更新:

模擬器的提示訊息也可以看到 API 的位址,於另一個終端機畫面上試戳 API:

# functions 在本地的運行的位置應為 http://127.0.0.1:5001/your-project-id/your-server-region/counter
# 在其結尾加入 /number 後作為 API 的位置
# 戳完以後,正常可以看到資料庫的數字 +1 了
curl -X POST http://127.0.0.1:5001/your-project-id/your-server-region/counter/number
# {"data":4}

部署

當本地開發完成,測試也都如預期後,firebase 方便的一條指令就能將 functions 及 database.rules.json 部署到雲端:

firebase deploy

前端

為了在讓網站前端能使用 Firebase,需要註冊一個前端的應用程式,並取得相關的設定值,以下畫面在 Firebase 的專案畫面中可以找到, 點擊「新增應用程式」後依照指示完成註冊:

使用 Vue 來渲染資料

以 Vue 為例,安裝 firebase 後,使用 Firebase Realtime database 的範例:

import { initializeApp } from "firebase/app";
import { ref } from "vue";
import { getDatabase, ref as databaseRef, onValue } from "firebase/database";
import { getFunctions, httpsCallable } from "firebase/functions";

export const firebaseApp = initializeApp({
  // 使用你的應用程式設定...
});

export const db = getDatabase(firebaseApp);
export const functions = getFunctions(firebaseApp);

export const getDatabaseRef = path => {
  const data = ref(null);
  const reference = databaseRef(db, path);
  onValue(reference, snapshot => {
    data.value = snapshot.val();
  });
  return data;
}

export const incrementCount = httpsCallable(functions, "counter/number");
  • 這裡寫了一個工具函數 getDatabaseRef,可以傳入一個 Database 的資料路徑, 回傳一個 Vueref 物件(注意不是 Firebase 的 ref)。
  • 使用 onValue 監聽資料變動,callback function 會在第一次取得資料以及之後有資料變更時被呼叫,藉此改變 Vue ref 的值。
  • onValue 是新版 JS SDK 的用法,這與幾年前學過舊版 DatabaseRef.on("value", ...) 用法不一樣了。
  • +1 的功能我們可以用 axios + API Endpoint 來實作,但 firebase js SDK 已經提供了方便的功能讓我們直接調用 functions, 這裡使用了 httpsCallable 建立與 API 的連結,所產生的結果可以作為函數直接呼叫使用。

接著在 Vue 中可以這樣使用:

<template>
  {{ number }}
  <button @click="incrementCount">+1</button>
</template>
<script setup>
import { getDatabaseRef, incrementCount } from "./firebase";
const number = getDatabaseRef("number");
</script> 

結語

到目前為止完成了一個簡單的計數功能,可以在前端透過 API 調用來 +1,並且畫面上的數字回即時響應數字的更新。 Firebase 可以讓我們快速開發出一些簡單的功能,而不需要自己架設伺服器,很適合用在一些簡單情境作為輔助, 而且計費方式為 Pay as you go,免費額度(每月 200萬次 function calls、每天 360MB DB 下載流量、1GB 的 DB 空間)也足以滿足大部分的需求。

Firebase 的主要功能 functions 其實跟 AWS lambda 就是相似的東西, 我個人的體驗是 functions 用起來更方便許多,然後 functions 與 database 是同一個專案下的不同資源, 在 functions 當中可以直接使用 database 光是這點就是很大的賣點了,再來部署方面也比 AWS lambda 簡單快速多了。

關於 Firebase functions 以及 Realtime database 有些沒介紹到也值得提出的部分:

  • Firebase functions 是產品,背後是用 Google Cloud Functions 包裝,可以因應需求設置自動擴展、timeout、記憶體限制、環境變數、Secret。
  • Firebase functions 也支援排程 Schedule functions,直接在程式寫好時間設定,部署後就可以讓他定時自己執行。
  • Cloud Functions 目前有兩個版本一、二代的差異
  • Firebase JS SDK 有版本 9 及版本 8 的使用差異,早期學到的用法都是版本 8。
  • 同專案下可以有多個 Realtime database 資料庫,可以有各自的規則(database.rules.json),不論在前端或 functions 都可以調用不同的資料庫。

留言