带有 Firestore(Firebase)的 API 服务器
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", };
-
在管理控制台的
身份验证
下,您需要启用电子邮件/密码
登录方法。 -
您需要在
身份验证
下添加一个用户和密码,然后添加用户
部分,记下以后使用的值。 -
将
Firestore 数据库
添加到您的项目中。控制台将允许您在生产模式或测试模式下进行设置。您可以根据需要进行配置,但生产模式需要您进一步设置安全规则。 -
在数据库中添加一个名为
songs
的集合。这将要求您添加至少一个文档。只需使用自动 ID设置文档即可。
注意根据您的 Google 帐户状态,可能需要执行其他设置和管理步骤。
编写应用程序跳转到标题
我们希望在您最喜欢的编辑器中将我们的应用程序创建为 JavaScript 文件。
我们首先要导入 Firebase 在部署时需要使用的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,因此最好在部署下使用 Firebase 的 Web 库。目前 Firebase v9 仍处于测试阶段,因此在本教程中我们将使用 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