API 伺服器與 Firestore (Firebase)
Firebase 是 Google 開發的平台,用於建立行動和 Web 應用程式。您可以使用 Firestore 在平台上持久儲存資料。在本教學中,讓我們看看如何使用它來建構一個小型 API,該 API 具有插入和檢索資訊的端點。
總覽 跳到標題
我們將建構一個具有單一端點的 API,該端點接受 GET
和 POST
請求,並傳回資訊的 JSON 酬載
# A GET request to the endpoint without any sub-path should return the details
# of all songs in the store:
GET /songs
# response
[
{
title: "Song Title",
artist: "Someone",
album: "Something",
released: "1970",
genres: "country rap",
}
]
# A GET request to the endpoint with a sub-path to the title should return the
# details of the song based on its title.
GET /songs/Song%20Title # '%20' == space
# response
{
title: "Song Title"
artist: "Someone"
album: "Something",
released: "1970",
genres: "country rap",
}
# A POST request to the endpoint should insert the song details.
POST /songs
# post request body
{
title: "A New Title"
artist: "Someone New"
album: "Something New",
released: "2020",
genres: "country rap",
}
在本教學中,我們將會:
- 建立和設定 Firebase 專案。
- 使用文字編輯器建立我們的應用程式。
- 建立 gist 以「託管」我們的應用程式。
- 在 Deno Deploy 上部署我們的應用程式。
- 使用 cURL 測試我們的應用程式。
概念 跳到標題
有一些概念有助於理解為什麼我們在教學的其餘部分採用特定的方法,並且可以幫助擴展應用程式。如果您願意,可以跳到 設定 Firebase。
Deploy 類似於瀏覽器 跳到標題
即使 Deploy 在雲端中執行,但在許多方面,它提供的 API 都是基於 Web 標準。因此,當使用 Firebase 時,Firebase API 比那些為伺服器執行階段設計的 API 更相容於 Web。這表示在本教學中我們將使用 Firebase Web 程式庫。
Firebase 使用 XHR 跳到標題
Firebase 使用 Closure 的 WebChannel 的包裝器,而 WebChannel 最初是圍繞 XMLHttpRequest
建構的。雖然 WebChannel 支援更現代的 fetch()
API,但目前 Web 版 Firebase 的版本並未統一實例化具有 fetch()
支援的 WebChannel,而是使用 XMLHttpRequest
。
雖然 Deploy 類似於瀏覽器,但它不支援 XMLHttpRequest
。XMLHttpRequest
是一個「舊版」瀏覽器 API,它有幾個限制和功能難以在 Deploy 中實作,這表示 Deploy 不太可能實作該 API。
因此,在本教學中,我們將使用一個有限的 polyfill,它提供了足夠的 XMLHttpRequest
功能集,以允許 Firebase/WebChannel 與伺服器通訊。
Firebase 驗證 跳到標題
Firebase 提供了相當 多的驗證選項。在本教學中,我們將使用電子郵件和密碼驗證。
當使用者登入時,Firebase 可以持久儲存該驗證資訊。由於我們正在使用 Firebase 的 Web 程式庫,因此持久儲存驗證資訊允許使用者離開頁面,並且在返回時無需重新登入。Firebase 允許將驗證資訊持久儲存在本機儲存空間、工作階段儲存空間或不儲存。
在 Deploy 環境中,情況有點不同。Deploy 部署將保持「活動」狀態,這表示在某些請求中,記憶體狀態將在請求之間保持存在,但在各種條件下,可能會啟動或關閉新的部署。目前,Deploy 除了記憶體分配之外,不提供任何持久性。此外,它目前不提供全域 localStorage
或 sessionStorage
,而 Firebase 正是使用它們來儲存驗證資訊。
為了減少重新驗證的需求,同時確保我們可以使用單一部署支援多個使用者,我們將使用一個 polyfill,它將允許我們為 Firebase 提供 localStorage
介面,但將資訊儲存為用戶端中的 Cookie。
設定 Firebase 跳到標題
Firebase 是一個功能豐富的平台。Firebase 管理的所有詳細資訊都超出了本教學的範圍。我們將涵蓋本教學所需的內容。
-
在 Firebase 控制台下建立一個新專案。
-
將 Web 應用程式新增至您的專案。記下設定精靈中提供的
firebaseConfig
。它看起來應該像下面這樣。我們稍後會用到它firebase.jsvar firebaseConfig = { apiKey: "APIKEY", authDomain: "example-12345.firebaseapp.com", projectId: "example-12345", storageBucket: "example-12345.appspot.com", messagingSenderId: "1234567890", appId: "APPID", };
-
在管理控制台的
Authentication
下,您會想要啟用Email/Password
登入方法。 -
您會想要在
Authentication
,然後在Users
區段下新增使用者名稱和密碼,並記下稍後使用的值。 -
將
Firestore Database
新增至您的專案。控制台將允許您在生產模式或測試模式下設定。如何設定取決於您,但生產模式將要求您設定進一步的安全規則。 -
將集合新增至名為
songs
的資料庫。這將要求您至少新增一個文件。只需使用自動 ID 設定文件即可。
注意:根據您的 Google 帳戶狀態,可能需要執行其他設定和管理步驟。
編寫應用程式 跳到標題
我們想要在我們最愛的編輯器中將我們的應用程式建立為 JavaScript 檔案。
我們要做的第一件事是匯入 Firebase 需要在 Deploy 下運作的 XMLHttpRequest
polyfill,以及允許 Firebase 驗證持久儲存已登入使用者的 localStorage
polyfill
import "https://deno.land/x/xhr@0.1.1/mod.ts";
import { installGlobals } from "https://deno.land/x/virtualstorage@0.1.0/mod.ts";
installGlobals();
ℹ️ 我們正在使用撰寫本教學時套件的目前版本。它們可能不是最新的,您可能需要再次檢查目前版本。
由於 Deploy 具有許多 Web 標準 API,因此最好在 Deploy 下使用 Firebase 的 Web 程式庫。目前 v9 仍在 Firebase 的 Beta 版中,因此我們將在本教學中使用 v8
import firebase from "https://esm.sh/firebase@8.7.0/app";
import "https://esm.sh/firebase@8.7.0/auth";
import "https://esm.sh/firebase@8.7.0/firestore";
我們也將使用 oak 作為中介軟體框架來建立 API,包括將 localStorage
值設為用戶端 Cookie 的中介軟體
import {
Application,
Router,
Status,
} from "https://deno.land/x/oak@v7.7.0/mod.ts";
import { virtualStorage } from "https://deno.land/x/virtualstorage@0.1.0/middleware.ts";
現在我們需要設定我們的 Firebase 應用程式。我們將從環境變數中取得組態,我們稍後將在金鑰 FIREBASE_CONFIG
下設定這些環境變數,並取得對我們要使用的 Firebase 部分的參考
const firebaseConfig = JSON.parse(Deno.env.get("FIREBASE_CONFIG"));
const firebaseApp = firebase.initializeApp(firebaseConfig, "example");
const auth = firebase.auth(firebaseApp);
const db = firebase.firestore(firebaseApp);
我們也將設定應用程式以處理每個請求的已登入使用者。因此,我們將建立一個使用者對應,其中包含我們先前在此部署中登入的使用者。雖然在本教學中,我們只會有一個已登入使用者,但可以輕鬆地調整程式碼以允許用戶端個別登入
const users = new Map();
讓我們建立我們的中介軟體路由器,並建立三個不同的中介軟體處理常式,以支援 /songs
的 GET
和 POST
,以及 /songs/{title}
上特定歌曲的 GET
const router = new Router();
// Returns any songs in the collection
router.get("/songs", async (ctx) => {
const querySnapshot = await db.collection("songs").get();
ctx.response.body = querySnapshot.docs.map((doc) => doc.data());
ctx.response.type = "json";
});
// Returns the first document that matches the title
router.get("/songs/:title", async (ctx) => {
const { title } = ctx.params;
const querySnapshot = await db.collection("songs").where("title", "==", title)
.get();
const song = querySnapshot.docs.map((doc) => doc.data())[0];
if (!song) {
ctx.response.status = 404;
ctx.response.body = `The song titled "${ctx.params.title}" was not found.`;
ctx.response.type = "text";
} else {
ctx.response.body = querySnapshot.docs.map((doc) => doc.data())[0];
ctx.response.type = "json";
}
});
function isSong(value) {
return typeof value === "object" && value !== null && "title" in value;
}
// Removes any songs with the same title and adds the new song
router.post("/songs", async (ctx) => {
const body = ctx.request.body();
if (body.type !== "json") {
ctx.throw(Status.BadRequest, "Must be a JSON document");
}
const song = await body.value;
if (!isSong(song)) {
ctx.throw(Status.BadRequest, "Payload was not well formed");
}
const querySnapshot = await db
.collection("songs")
.where("title", "==", song.title)
.get();
await Promise.all(querySnapshot.docs.map((doc) => doc.ref.delete()));
const songsRef = db.collection("songs");
await songsRef.add(song);
ctx.response.status = Status.NoContent;
});
好的,我們快完成了。我們只需要建立我們的中介軟體應用程式,並新增我們匯入的 localStorage
中介軟體
const app = new Application();
app.use(virtualStorage());
然後我們需要新增中介軟體來驗證使用者。在本教學中,我們只是從我們將設定的環境變數中抓取使用者名稱和密碼,但這可以很容易地調整為將使用者重新導向到登入頁面(如果他們未登入)
app.use(async (ctx, next) => {
const signedInUid = ctx.cookies.get("LOGGED_IN_UID");
const signedInUser = signedInUid != null ? users.get(signedInUid) : undefined;
if (!signedInUid || !signedInUser || !auth.currentUser) {
const creds = await auth.signInWithEmailAndPassword(
Deno.env.get("FIREBASE_USERNAME"),
Deno.env.get("FIREBASE_PASSWORD"),
);
const { user } = creds;
if (user) {
users.set(user.uid, user);
ctx.cookies.set("LOGGED_IN_UID", user.uid);
} else if (signedInUser && signedInUid.uid !== auth.currentUser?.uid) {
await auth.updateCurrentUser(signedInUser);
}
}
return next();
});
現在讓我們將我們的路由器新增至中介軟體應用程式,並將應用程式設定為監聽埠 8000
app.use(router.routes());
app.use(router.allowedMethods());
await app.listen({ port: 8000 });
現在我們有一個應該提供我們 API 的應用程式。
部署應用程式 跳到標題
現在我們已準備就緒,讓我們部署您的新應用程式!
- 在您的瀏覽器中,造訪 Deno Deploy 並連結您的 GitHub 帳戶。
- 選取包含您的新應用程式的儲存庫。
- 您可以為您的專案命名,或允許 Deno 為您產生一個名稱
- 在 Entrypoint 下拉式選單中選取
firebase.js
- 按一下部署專案
為了使您的應用程式運作,我們需要設定其環境變數。
在您專案的成功頁面或專案儀表板中,按一下新增環境變數。在環境變數下,按一下 + 新增變數。建立以下變數
FIREBASE_USERNAME
- 上面新增的 Firebase 使用者(電子郵件地址)FIREBASE_PASSWORD
- 上面新增的 Firebase 使用者密碼FIREBASE_CONFIG
- Firebase 應用程式的組態,以 JSON 字串表示
組態需要是有效的 JSON 字串,才能讓應用程式讀取。如果設定時給定的程式碼片段看起來像這樣
var firebaseConfig = {
apiKey: "APIKEY",
authDomain: "example-12345.firebaseapp.com",
projectId: "example-12345",
storageBucket: "example-12345.appspot.com",
messagingSenderId: "1234567890",
appId: "APPID",
};
您需要將字串的值設定為這樣(請注意,不需要空格和換行符號)
{
"apiKey": "APIKEY",
"authDomain": "example-12345.firebaseapp.com",
"projectId": "example-12345",
"storageBucket": "example-12345.appspot.com",
"messagingSenderId": "1234567890",
"appId": "APPID"
}
按一下以儲存變數。
現在讓我們試用一下我們的 API。
我們可以建立一首新歌
curl --request POST \
--header "Content-Type: application/json" \
--data '{"title": "Old Town Road", "artist": "Lil Nas X", "album": "7", "released": "2019", "genres": "Country rap, Pop"}' \
--dump-header \
- https://<project_name>.deno.dev/songs
我們可以取得我們集合中的所有歌曲
curl https://<project_name>.deno.dev/songs
我們可以取得關於我們建立的標題的特定資訊
curl https://<project_name>.deno.dev/songs/Old%20Town%20Road