使用 Cloud Functions 扩展 Realtime Database


借助 Cloud Functions,您可以处理 Firebase Realtime Database 中的事件,而无需更新客户端代码。Cloud Functions 让您能以完整的管理员权限执行 Realtime Database 操作,并可确保对 Realtime Database 所做的每项更改都得到单独处理。您可以通过 DataSnapshotAdmin SDK 执行 Firebase Realtime Database 更改。

在典型生命周期中,Firebase Realtime Database 函数会执行以下操作:

  1. 等待对特定 Realtime Database 位置执行的更改。
  2. 在事件发生时触发并执行相应的任务(如需查看更多使用场景示例,请参阅 Cloud Functions 有哪些用途?)。
  3. 接收相关数据对象,该对象包含了存储在指定文档中的数据的快照。

触发 Realtime Database 函数

您可以使用 functions.database 为 Realtime Database 事件创建新函数。如需控制函数何时触发,请指定一个事件处理程序,并指定要监听事件的 Realtime Database 路径。

设置事件处理程序

函数可让您以两种不同的监听范围处理 Realtime Database 事件;您可以只监听创建、更新或删除事件,也可以监听某个路径上任何类型的任何更改。 对于 Realtime Database,Cloud Functions 支持以下事件处理程序:

  • onWrite():当 Realtime Database 中发生数据创建、更新或删除事件时触发。
  • onCreate():当 Realtime Database 中发生新数据创建事件时触发。
  • onUpdate():当 Realtime Database 中发生数据更新事件时触发。
  • onDelete():当 Realtime Database 中发生数据删除事件时触发。

指定实例和路径

如需控制函数的触发时间和位置,请调用 ref(path) 以指定路径,并视需要使用 instance('INSTANCE_NAME') 指定 Realtime Database 实例。如果您没有指定实例,函数将部署到 Firebase 项目的默认 Realtime Database 实例。例如:

  • 默认 Realtime Database 实例:functions.database.ref('/foo/bar')
  • 名为“my-app-db-2”的实例:functions.database.instance('my-app-db-2').ref('/foo/bar')

这些方法用于指示您的函数处理在 Realtime Database 实例中特定路径下的写入操作。根据路径规范,涉及该路径的所有写入操作均匹配(包括在此路径下任何位置发生的写入操作)。如果您将函数的路径设置为 /foo/bar,则以下两个位置发生的事件均匹配:

 /foo/bar
 /foo/bar/baz/really/deep/path

不管是哪种情况,Firebase 都会解读为事件是在 /foo/bar 下发生的,且事件数据包括 /foo/bar 下的旧数据和新数据。如果事件数据可能很大,请考虑在更深的路径下使用多个函数,而不是在靠近数据库根目录处使用单个函数。为了获得最佳性能,请仅在尽可能深的路径层级中发出数据请求。

您可以用大括号括住路径的某个部分,将其指定为通配符;ref('foo/{bar}') 会匹配 /foo 的任何子路径。这些通配符路径组成部分的值可从函数的 EventContext.params 对象中获得。在此示例中,该值通过 context.params.bar 提供。

包含通配符的路径可以匹配单个写入操作引发的多个事件。 下列代码的一次插入

{
  "foo": {
    "hello": "world",
    "firebase": "functions"
  }
}

会匹配路径 "/foo/{bar}" 两次:第一次是 "hello": "world",第二次是 "firebase": "functions"

处理事件数据

处理 Realtime Database 事件时,返回的数据对象是一个 DataSnapshot。对于 onWriteonUpdate 事件,第一个参数是一个 Change 对象,其中包含代表触发事件前后的数据状态的两个快照。对于 onCreateonDelete 事件,返回的数据对象是已创建或已删除的数据的快照。

在以下示例中,函数会检索指定路径的快照,将该位置中的字符串转换为大写,并将修改后的字符串写入数据库:

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snapshot, context) => {
      // Grab the current value of what was written to the Realtime Database.
      const original = snapshot.val();
      functions.logger.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      return snapshot.ref.parent.child('uppercase').set(uppercase);
    });

访问用户身份验证信息

EventContext.authEventContext.authType 中,您可以访问触发函数的用户的信息(包括权限)。这对于实施安全规则很有用,可以让您的函数根据用户的权限级别完成不同的操作:

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

exports.simpleDbFunction = functions.database.ref('/path')
    .onCreate((snap, context) => {
      if (context.authType === 'ADMIN') {
        // do something
      } else if (context.authType === 'USER') {
        console.log(snap.val(), 'written by', context.auth.uid);
      }
    });

此外,您可以利用用户身份验证信息来“模拟”用户并代表用户执行写入操作。请务必按如下所示删除该应用实例,以防出现并发问题:

exports.impersonateMakeUpperCase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snap, context) => {
      const appOptions = JSON.parse(process.env.FIREBASE_CONFIG);
      appOptions.databaseAuthVariableOverride = context.auth;
      const app = admin.initializeApp(appOptions, 'app');
      const uppercase = snap.val().toUpperCase();
      const ref = snap.ref.parent.child('uppercase');

      const deleteApp = () => app.delete().catch(() => null);

      return app.database().ref(ref).set(uppercase).then(res => {
        // Deleting the app is necessary for preventing concurrency leaks
        return deleteApp().then(() => res);
      }).catch(err => {
        return deleteApp().then(() => Promise.reject(err));
      });
    });

读取先前的值

Change 对象有一个 before 属性,可用于检查在事件发生之前保存在 Realtime Database 中的内容。before 属性会返回一个 DataSnapshot,其中所有方法(例如 val()exists())都会引用先前的值。您可以通过以下两种方法再次读取新的值:使用原始 DataSnapshot 或读取 after 属性。任何 Change 对象的此属性都是另一个 DataSnapshot,代表事件发生后的数据状态。

例如,before 属性可用于确保函数仅在第一次创建记录时将文本改为大写:

exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onWrite((change, context) => {
      // Only edit data when it is first created.
      if (change.before.exists()) {
        return null;
      }
      // Exit when the data is deleted.
      if (!change.after.exists()) {
        return null;
      }
      // Grab the current value of what was written to the Realtime Database.
      const original = change.after.val();
      console.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      return change.after.ref.parent.child('uppercase').set(uppercase);
    });