在本页

使用 Firestore(Firebase)的 API 服务器

Firebase 是 Google 开发的用于创建移动和 Web 应用程序的平台。您可以使用 Firestore 在平台上持久化数据。在本教程中,让我们看看如何使用它来构建一个具有插入和检索信息的端点的简单 API。

概述 跳转到标题

我们将构建一个具有单个端点的 API,该端点接受 GETPOST 请求并返回包含信息的 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 与 Web 的兼容性比为服务器运行时设计的 API 更好。这意味着在本教程中,我们将使用 Firebase Web 库。

Firebase 使用 XHR 跳转到标题

Firebase 使用了 Closure 的 WebChannel 的包装器,而 WebChannel 最初是基于 XMLHttpRequest 构建的。虽然 WebChannel 支持更现代的 fetch() API,但当前版本的 Web 版 Firebase 并没有统一地使用 fetch() 支持来实例化 WebChannel,而是使用 XMLHttpRequest

虽然 Deploy 类似于浏览器,但它不支持 XMLHttpRequestXMLHttpRequest 是一个“遗留”的浏览器 API,它有一些限制和功能,在 Deploy 中很难实现,这意味着 Deploy 不太可能实现该 API。

因此,在本教程中,我们将使用一个有限的polyfill,它提供了足够的 XMLHttpRequest 功能集,以允许 Firebase/WebChannel 与服务器通信。

Firebase 身份验证 跳转到标题

Firebase 提供了相当多的 身份验证选项。在本教程中,我们将使用电子邮件和密码身份验证。

当用户登录时,Firebase 可以持久保存该身份验证。因为我们使用的是 Firebase 的 Web 库,所以持久保存身份验证允许用户离开页面,并在返回时无需重新登录。Firebase 允许身份验证持久保存到本地存储、会话存储或都不保存。

在 Deploy 上下文中,情况略有不同。Deploy 部署将保持“活动”状态,这意味着内存中的状态将在某些请求的请求之间存在,但在各种情况下,可以启动或关闭新的部署。目前,Deploy 除了内存分配之外,没有提供任何持久性。此外,它目前不提供全局 localStoragesessionStorage,而 Firebase 使用它们来存储身份验证信息。

为了减少重新身份验证的需要,同时确保我们能够使用单个部署支持多个用户,我们将使用一个 polyfill,它将允许我们为 Firebase 提供 localStorage 接口,但将信息存储为客户端的 cookie。

设置 Firebase 跳转到标题

Firebase 是一个功能丰富的平台。Firebase 管理的所有细节超出了本教程的范围。我们将介绍本教程所需的知识。

  1. Firebase 控制台 中创建一个新项目。

  2. 将 Web 应用程序添加到您的项目中。记下设置向导中提供的 firebaseConfig。它应该类似于以下内容。我们稍后会使用它。

    firebase.js
    var firebaseConfig = {
      apiKey: "APIKEY",
      authDomain: "example-12345.firebaseapp.com",
      projectId: "example-12345",
      storageBucket: "example-12345.appspot.com",
      messagingSenderId: "1234567890",
      appId: "APPID",
    };
    
  3. 在管理控制台的 身份验证 下,您需要启用 电子邮件/密码 登录方法。

  4. 您需要在 身份验证用户 部分添加一个用户和密码,并记下稍后使用的值。

  5. Firestore 数据库 添加到您的项目中。控制台将允许您在生产模式测试模式下进行设置。您可以根据自己的需要进行配置,但生产模式将要求您进一步设置安全规则。

  6. 在数据库中添加一个名为 songs 的集合。这将要求您添加至少一个文档。只需使用自动 ID 设置文档即可。

注意根据您的 Google 帐户状态,可能需要执行其他设置和管理步骤。

编写应用程序 跳转到标题

我们希望在最喜欢的编辑器中将应用程序创建为 JavaScript 文件。

我们要做的第一件事是导入 Firebase 在 Deploy 下工作所需的 XMLHttpRequest polyfill,以及 localStorage 的 polyfill,以允许 Firebase 身份验证持久保存已登录的用户。

firebase.js
import "http://land.deno.org.cn/x/[email protected]/mod.ts";
import { installGlobals } from "http://land.deno.org.cn/x/[email protected]/mod.ts";
installGlobals();

ℹ️ 我们使用的是本教程编写时当前版本的软件包。它们可能不是最新的,您可能需要仔细检查当前版本。

由于 Deploy 具有许多 Web 标准 API,因此最好在部署下使用 Firebase 的 Web 库。目前,v9 仍处于 Firebase 的测试阶段,因此在本教程中我们将使用 v8。

firebase.js
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 设置的中间件。

firebase.js
import {
  Application,
  Router,
  Status,
} from "http://land.deno.org.cn/x/[email protected]/mod.ts";
import { virtualStorage } from "http://land.deno.org.cn/x/[email protected]/middleware.ts";

现在我们需要设置我们的 Firebase 应用程序。我们将从环境变量中获取配置,这些环境变量将在稍后在键 FIREBASE_CONFIG 下设置,并获取对我们将要使用的 Firebase 部分的引用。

firebase.js
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);

我们还将设置应用程序以处理每个请求的已登录用户。因此,我们将创建一个用户映射,这些用户是我们之前在此部署中登录的。虽然在本教程中,我们只会有一个已登录用户,但代码可以轻松地适应以允许客户端单独登录。

firebase.js
const users = new Map();

让我们创建我们的中间件路由器,并创建三个不同的中间件处理程序来支持 /songsGETPOST 以及 /songs/{title} 上特定歌曲的 GET

firebase.js
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 中间件。

firebase.js
const app = new Application();
app.use(virtualStorage());

然后我们需要添加中间件来验证用户。在本教程中,我们只是从我们将要设置的环境变量中获取用户名和密码,但这可以轻松地适应,如果用户未登录,则将其重定向到登录页面。

firebase.js
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。

firebase.js
app.use(router.routes());
app.use(router.allowedMethods());
await app.listen({ port: 8000 });

现在我们有一个应用程序,应该可以提供我们的 API。

部署应用程序 跳转到标题

现在我们已经准备就绪,让我们部署您的新应用程序!

  1. 在您的浏览器中,访问 Deno Deploy 并链接您的 GitHub 帐户。
  2. 选择包含您的新应用程序的存储库。
  3. 您可以为您的项目命名,也可以让 Deno 为您生成一个名称。
  4. 在入口点下拉菜单中选择 firebase.js
  5. 单击**部署项目**。

为了使您的应用程序正常工作,我们需要配置其环境变量。

在您的项目的成功页面或项目仪表板中,单击**添加环境变量**。在环境变量下,单击**+ 添加变量**。创建以下变量

  1. FIREBASE_USERNAME - 以上添加的 Firebase 用户(电子邮件地址)
  2. FIREBASE_PASSWORD - 以上添加的 Firebase 用户密码
  3. 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