使用 Cloud Functions 扩展 Firestore

借助 Cloud Functions,您可以部署 Node.js 代码来处理因 Firestore 数据库更改而触发的事件。这样您就可以轻松向您的应用中添加服务器端功能,而无需运行您自己的服务器。

如需用例的示例,请参阅 Cloud Functions 有哪些用途?或 GitHub 代码库中的 Functions 示例

版本和模式要求

本文档适用于原生模式下的 Firestore 标准版。

Firestore 企业版不支持 Cloud Functions(第 1 代)。请改用 Cloud Run functions(第 2 代)

Firestore 函数触发器

Cloud Functions for Firebase SDK 会导出 functions.firestore 对象,以便您创建与特定 Firestore 事件关联的处理程序。

事件类型 触发器
onCreate 首次写入某个文档时触发。
onUpdate 当某文档已存在且值发生了任何更改时触发。
onDelete 当包含数据的文档被删除时触发。
onWrite 在触发 onCreateonUpdateonDelete 时触发。

如果您还没有启用了 Cloud Functions for Firebase 的项目,请参阅《使用入门:编写和部署您的第一批函数》,配置并设置 Cloud Functions for Firebase 项目。

编写 Firestore 触发的函数

定义函数触发器

要定义 Firestore 触发器,请指定文档路径和事件类型:

Node.js

const functions = require('firebase-functions');

exports.myFunction = functions.firestore
  .document('my-collection/{docId}')
  .onWrite((change, context) => { /* ... */ });

文档路径可以引用特定文档通配符模式

指定单个文档

如果您希望针对特定文档的任何更改都触发一个事件,可以使用以下函数。

Node.js

// Listen for any change on document `marie` in collection `users`
exports.myFunctionName = functions.firestore
    .document('users/marie').onWrite((change, context) => {
      // ... Your code here
    });

使用通配符指定一组文档

如果您要将触发器附加到一组文档(例如特定集合中的任何文档)中,请使用 {wildcard} 替代文档 ID:

Node.js

// Listen for changes in all documents in the 'users' collection
exports.useWildcard = functions.firestore
    .document('users/{userId}')
    .onWrite((change, context) => {
      // If we set `/users/marie` to {name: "Marie"} then
      // context.params.userId == "marie"
      // ... and ...
      // change.after.data() == {name: "Marie"}
    });

在此示例中,如果 users 中的任何文档的任何字段发生更改,都会匹配一个名为 userId 的通配符。

如果 users 中的某个文档有多个子集合,并且这些子集合中一个文档内的某字段发生了更改,则不会触发 userId 通配符。

通配符匹配项会从文档路径中提取出来并存储到 context.params 中。您可以定义任意多个通配符,以替代明确指定的集合或文档 ID,例如:

Node.js

// Listen for changes in all documents in the 'users' collection and all subcollections
exports.useMultipleWildcards = functions.firestore
    .document('users/{userId}/{messageCollectionId}/{messageId}')
    .onWrite((change, context) => {
      // If we set `/users/marie/incoming_messages/134` to {body: "Hello"} then
      // context.params.userId == "marie";
      // context.params.messageCollectionId == "incoming_messages";
      // context.params.messageId == "134";
      // ... and ...
      // change.after.data() == {body: "Hello"}
    });

事件触发器

在创建新文档时触发函数

若想在集合中创建新文档时触发一个函数,您可以使用带通配符onCreate() 处理程序来实现。每次添加了新的用户个人资料时,此示例函数都会调用 createUser

Node.js

exports.createUser = functions.firestore
    .document('users/{userId}')
    .onCreate((snap, context) => {
      // Get an object representing the document
      // e.g. {'name': 'Marie', 'age': 66}
      const newValue = snap.data();

      // access a particular field as you would any JS property
      const name = newValue.name;

      // perform desired operations ...
    });

当文档更新时触发函数

若想在更新文档时触发一个函数,您也可以使用带通配符onUpdate() 函数来实现。此示例函数会在用户更改其个人资料时调用 updateUser

Node.js

exports.updateUser = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Get an object representing the document
      // e.g. {'name': 'Marie', 'age': 66}
      const newValue = change.after.data();

      // ...or the previous value before this update
      const previousValue = change.before.data();

      // access a particular field as you would any JS property
      const name = newValue.name;

      // perform desired operations ...
    });

在删除文档时触发函数

若想在删除文档时触发一个函数,您也可以使用带通配符onDelete() 函数来实现。当用户删除其个人资料时,此示例函数会调用 deleteUser

Node.js

exports.deleteUser = functions.firestore
    .document('users/{userID}')
    .onDelete((snap, context) => {
      // Get an object representing the document prior to deletion
      // e.g. {'name': 'Marie', 'age': 66}
      const deletedValue = snap.data();

      // perform desired operations ...
    });

对文档进行任何更改时触发函数

如果您不在意触发的事件类型,则可以使用带通配符onWrite() 函数来监听 Firestore 文档中的所有更改。创建、更新或删除用户时,此示例函数会调用 modifyUser

Node.js

exports.modifyUser = functions.firestore
    .document('users/{userID}')
    .onWrite((change, context) => {
      // Get an object with the current document value.
      // If the document does not exist, it has been deleted.
      const document = change.after.exists ? change.after.data() : null;

      // Get an object with the previous document value (for update or delete)
      const oldDocument = change.before.data();

      // perform desired operations ...
    });

读取和写入数据

函数被触发后,会提供与相应事件相关的数据的快照。您可以使用此快照对触发了该事件的文档执行读取或写入操作,也可以使用 Firebase Admin SDK 访问数据库的其他部分。

事件数据

读取数据

函数触发后,您可能希望获取更新后文档的数据,或者获取更新之前的数据。您可以通过使用 change.before.data() 来获取之前的数据,其中包含更新前的文档快照。同样,change.after.data() 包含更新后的文档快照状态。

Node.js

exports.updateUser2 = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Get an object representing the current document
      const newValue = change.after.data();

      // ...or the previous value before this update
      const previousValue = change.before.data();
    });

您可以像在其他任何对象中一样访问属性。或者,您也可以使用 get 函数访问特定字段:

Node.js

// Fetch data using standard accessors
const age = snap.data().age;
const name = snap.data()['name'];

// Fetch data using built in accessor
const experience = snap.get('experience');

写入数据

每个函数调用都与 Firestore 数据库中的特定文档相关联。您可以在为函数返回的快照的 ref 属性中以 DocumentReference 的形式访问该文档。

DocumentReference 来自 Firestore Node.js SDK,并且包含 update()set()remove() 等方法,因此您可以轻松修改触发了函数的文档。

Node.js

// Listen for updates to any `user` document.
exports.countNameChanges = functions.firestore
    .document('users/{userId}')
    .onUpdate((change, context) => {
      // Retrieve the current and previous value
      const data = change.after.data();
      const previousData = change.before.data();

      // We'll only update if the name has changed.
      // This is crucial to prevent infinite loops.
      if (data.name == previousData.name) {
        return null;
      }

      // Retrieve the current count of name changes
      let count = data.name_change_count;
      if (!count) {
        count = 0;
      }

      // Then return a promise of a set operation to update the count
      return change.after.ref.set({
        name_change_count: count + 1
      }, {merge: true});
    });

触发事件之外的数据

Cloud Functions 函数在受信任的环境中执行,这意味着这些函数被授权为项目的服务账号。您可以使用 Firebase Admin SDK 执行读取和写入操作:

Node.js

const admin = require('firebase-admin');
admin.initializeApp();

const db = admin.firestore();

exports.writeToFirestore = functions.firestore
  .document('some/doc')
  .onWrite((change, context) => {
    db.doc('some/otherdoc').set({ ... });
  });

限制

请注意适用于 Cloud Run functions 的 Firestore 触发器的以下限制:

  • Cloud Run functions (第 1 代) 前提条件是 Firestore 原生模式的现有“(默认)”数据库。它不支持 Firestore 命名数据库或 Datastore 模式。在这种情况下,请使用 Cloud Run functions (第 2 代) 来配置事件。
  • 对 Cloud Run functions 和 Firestore 触发器进行跨项目设置存在限制。如需设置 Firestore 触发器,Cloud Run functions 必须位于同一项目中。
  • 无法保证顺序。快速更改会以意想不到的顺序触发函数调用。
  • 事件至少会被传送一次,但单个事件可能会导致多次调用函数。应该避免依赖“正好一次”机制,并编写幂等函数
  • Datastore 模式 Firestore 需要 Cloud Run functions(第 2 代)。Cloud Run functions(第 1 代)不支持 Datastore 模式。
  • 一个触发器与单一数据库相关联。您无法创建与多个数据库匹配的触发器。
  • 删除数据库不会自动删除该数据库的任何触发器。触发器会停止传送事件,但会继续存在,直到您删除触发器
  • 如果匹配的事件超过请求大小上限,该事件可能不会传送到 Cloud Run functions (第 1 代)。
    • 因请求大小而未传送的事件会记录在平台日志中,并计入项目的日志使用量。
    • 您可以在 Logs Explorer 中找到这些日志,其严重性为 error 且内容为“由于大小超出第 1 代的限制,因此事件无法传送到 Cloud Functions 函数”消息。您可以在 functionName 字段下方找到函数名称。如果 receiveTimestamp 字段仍在从现在起的一小时内,您可以利用该时间戳之前和之后的快照来读取相关文档,从而推断实际事件内容。
    • 为避免这种情况发生,您可以:
      • 迁移和升级到 Cloud Run functions (第 2 代)
      • 缩小文档
      • 删除相关的 Cloud Run functions 函数
    • 您可以使用排除功能关闭日志记录功能本身,但请注意,违规事件仍然不会传送。