使用 Firestore (Firebase) 的 API 服务器
Firebase 是 Google 开发的用于创建移动和 Web 应用程序的平台。您可以使用 Firestore 在平台上持久化数据。在本教程中,让我们看看如何使用它来构建一个小型 API,该 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 在云中运行,但在许多方面,它提供的 API 都是基于 Web 标准的。因此,当使用 Firebase 时,Firebase API 比那些为服务器运行时设计的 API 更兼容 Web。这意味着我们将在本教程中使用 Firebase Web 库。
Firebase 使用 XHR 跳转到标题
Firebase 使用 Closure WebChannel 的包装器,而 WebChannel 最初是围绕 XMLHttpRequest
构建的。虽然 WebChannel 支持更现代的 fetch()
API,但当前版本的 Firebase Web 并不统一使用 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,以及 localStorage
的 polyfill,以允许 Firebase 身份验证持久化登录用户
import "https://deno.land/x/[email protected]/mod.ts";
import { installGlobals } from "https://deno.land/x/[email protected]/mod.ts";
installGlobals();
ℹ️ 我们正在使用编写本教程时的软件包的当前版本。它们可能不是最新的,您可能需要仔细检查当前版本。
由于 Deploy 具有许多 Web 标准 API,因此最好在 Deploy 下使用 Firebase 的 Web 库。目前 v9 仍然是 Firebase 的 Beta 版,因此我们将在本教程中使用 v8
import firebase from "https://esm.sh/[email protected]/app";
import "https://esm.sh/[email protected]/auth";
import "https://esm.sh/[email protected]/firestore";
我们还将使用 oak 作为中间件框架来创建 API,包括中间件,该中间件将获取 localStorage
值并将其设置为客户端 Cookie
import {
Application,
Router,
Status,
} from "https://deno.land/x/[email protected]/mod.ts";
import { virtualStorage } from "https://deno.land/x/[email protected]/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 为您生成一个名称
- 在“入口点”下拉列表中选择
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