使用 Cloud Messaging 和 Cloud Functions 为 Web 应用发送通知

1. 概览

在此 Codelab 中,您将学习如何使用 Cloud Functions for Firebase 向聊天 Web 应用的用户发送通知,从而为该聊天应用添加功能。

3b1284f5144b54f6

学习内容

  • 使用 Firebase SDK 创建 Google Cloud Functions 函数。
  • 根据 Auth、Cloud Storage 和 Cloud Firestore 事件触发 Cloud Functions 函数。
  • 为您的 Web 应用添加 Firebase Cloud Messaging 支持。

所需条件

  • 信用卡。Cloud Functions for Firebase 要求使用 Firebase Blaze 方案,这意味着您必须使用信用卡为 Firebase 项目启用结算功能。
  • 您选择的 IDE/文本编辑器,例如 WebStormAtomSublime
  • 已安装 NodeJS v9 的终端,用于运行 shell 命令。
  • 浏览器,例如 Chrome。
  • 示例代码。请参阅后续步骤。

2. 获取示例代码

从命令行克隆 GitHub 代码库

git clone https://github.com/firebase/friendlychat

导入 starter 应用

使用 IDE 打开或导入示例代码目录中的 android_studio_folder.pngcloud-functions-start 目录。此目录包含此 Codelab 的起始代码,其中包含一个功能齐全的 Chat Web 应用。

3. 创建 Firebase 项目并设置您的应用

创建项目

Firebase 控制台中,点击 Add Project(添加项目)并将其命名为 FriendlyChat

点击创建项目

升级到 Blaze 方案

要使用 Cloud Functions for Firebase,您必须将 Firebase 项目升级为 Blaze 结算方案。这需要您向 Google Cloud 帐号添加信用卡或其他付款方式。

所有 Firebase 项目(包括使用 Blaze 方案的项目)仍然可以享受 Cloud Functions 的免费用量配额。此 Codelab 概述的步骤将在免费层级的使用量限额内。但是,您会看到用于托管 Cloud Functions 构建映像的 Cloud Storage 产生少量费用(约 $0.03)。

如果您无法使用信用卡或者不想继续使用 Blaze 方案,不妨考虑使用 Firebase Emulator Suite,它可让您在本地机器上免费模拟 Cloud Functions。

启用 Google 身份验证

为了让用户能够登录应用,我们将使用需要启用的 Google 身份验证。

在 Firebase 控制台中,依次打开构建部分 > 身份验证 > 登录方法标签页(或点击此处转到该标签页)。然后,启用 Google 登录提供方并点击保存。这样,用户就可以使用他们的 Google 账号登录该 Web 应用。

此外,您可以随时将应用的公开名称设置为 Friendly Chat

8290061806aacb46

启用 Cloud Storage

该应用使用 Cloud Storage 上传图片。如需在 Firebase 项目中启用 Cloud Storage,请访问存储部分,然后点击开始使用按钮。按照该位置中的步骤操作,Cloud Storage 位置会有一个要使用的默认值。随后点击完成

添加 Web 应用

在 Firebase 控制台上,添加一个 Web 应用。为此,请转到项目设置并向下滚动至添加应用。选择 Web 作为平台,并勾选用于设置 Firebase Hosting 的复选框,然后注册该应用并点击下一步完成其余步骤,最后点击前往控制台

4.安装 Firebase 命令行界面

借助 Firebase 命令行界面 (CLI),您可以在本地提供 Web 应用,并部署您的 Web 应用和 Cloud Functions。

如需安装或升级 CLI,请运行以下 npm 命令:

npm -g install firebase-tools

如需验证 CLI 是否已正确安装,请打开控制台并运行以下命令:

firebase --version

确保 Firebase CLI 版本高于 4.0.0,以使其具备 Cloud Functions 所需的所有最新功能。如果没有,请运行 npm install -g firebase-tools 进行升级,如上所示。

运行以下命令来授权 Firebase CLI:

firebase login

确保您位于 cloud-functions-start 目录中,然后设置 Firebase CLI 以使用您的 Firebase 项目:

firebase use --add

接下来,选择您的项目 ID 并按照说明操作。出现提示时,您可以选择任何别名,例如 codelab

5. 部署并运行 Web 应用

现在,您已导入并配置项目,可以首次运行 Web 应用了!打开一个终端窗口,转到 cloud-functions-start 文件夹,然后使用以下命令将 Web 应用部署到 Firebase Hosting:

firebase deploy --except functions

您应该会看到以下控制台输出:

i deploying database, storage, hosting
✔  database: rules ready to deploy.
i  storage: checking rules for compilation errors...
✔  storage: rules file compiled successfully
i  hosting: preparing ./ directory for upload...
✔  hosting: ./ folder uploaded successfully
✔ storage: rules file compiled successfully
✔ hosting: 8 files uploaded successfully
i starting release process (may take several minutes)...

✔ Deploy complete!

Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview
Hosting URL: https://friendlychat-1234.firebaseapp.com

打开 Web 应用

最后一行应该显示 Hosting 网址(托管网址)。现在,该 Web 应用应该会通过此网址(其格式应为 https://<项目 ID>.firebaseapp.com)提供。请打开此网址。您应该会看到聊天应用的正常运行界面。

使用 SIGN-IN WITH GOOGLE(使用 Google 帐号登录)按钮登录该应用,然后随时添加一些消息和发布图片:

3b1284f5144b54f6

如果您在新浏览器中首次登录该应用,请确保在系统提示时允许显示通知:8b9d0c66dc36153d

我们稍后需要启用通知功能。

如果您不小心点击了禁止,可以在 Chrome 多功能栏中点击网址左侧的 🔒? 安全按钮,然后切换通知旁边的栏来更改此设置:

e926868b0546ed71

现在,我们将使用 Firebase SDK for Cloud Functions 添加一些功能。

6. Functions 目录

借助 Cloud Functions,您无需设置服务器即可轻松实现在云端运行的代码。我们将介绍如何构建对 Firebase Authentication、Cloud Storage 和 Firebase Firestore 数据库事件做出反应的函数。我们先从身份验证开始。

使用 Firebase SDK for Cloud Functions 时,您的 Functions 代码将位于 functions 目录下(默认情况下)。您的 Functions 代码也是一个 Node.js 应用,因此需要一个 package.json 来提供有关您的应用的一些信息并列出依赖项。

为了方便您使用,我们已经为您创建了要放置代码的 functions/index.js 文件。在继续操作之前,您随时可以检查此文件。

cd functions
ls

如果您不熟悉 Node.js,在继续学习此 Codelab 之前,详细了解一下相关知识会很有帮助。

package.json 文件已列出两个必需的依赖项:Firebase SDK for Cloud FunctionsFirebase Admin SDK。如需在本地安装它们,请转到 functions 文件夹并运行以下命令:

npm install

现在,我们来看看 index.js 文件:

index.js

/**
 * Copyright 2017 Google Inc. All Rights Reserved.
 * ...
 */

// TODO(DEVELOPER): Import the Cloud Functions for Firebase and the Firebase Admin modules here.

// TODO(DEVELOPER): Write the addWelcomeMessage Function here.

// TODO(DEVELOPER): Write the blurImages Function here.

// TODO(DEVELOPER): Write the sendNotification Function here.

我们将导入所需的模块,然后编写三个函数来代替 TODO。我们先导入所需的节点模块。

7. 导入 Cloud Functions 和 Firebase Admin 模块

在此 Codelab 中,您需要使用两个模块:firebase-functions 支持写入 Cloud Functions 触发器和日志,而 firebase-admin 允许在具有管理员访问权限的服务器上使用 Firebase 平台来执行一些操作,例如写入 Cloud Firestore 或发送 FCM 通知。

index.js 文件中,将第一个 TODO 替换为以下内容:

index.js

/**
 * Copyright 2017 Google Inc. All Rights Reserved.
 * ...
 */

// Import the Firebase SDK for Google Cloud Functions.
const functions = require('firebase-functions');
// Import and initialize the Firebase Admin SDK.
const admin = require('firebase-admin');
admin.initializeApp();

// TODO(DEVELOPER): Write the addWelcomeMessage Function here.

// TODO(DEVELOPER): Write the blurImages Function here.

// TODO(DEVELOPER): Write the sendNotification Function here.

将 Firebase Admin SDK 部署到 Cloud Functions 环境或其他 Google Cloud Platform 容器后,系统会自动配置该 SDK,这发生在我们不使用任何参数调用 admin.initializeApp() 时。

现在,我们来添加一个当用户在聊天应用中首次登录时运行的函数。我们将添加一条欢迎用户的聊天消息。

8. 欢迎新用户

聊天消息结构

发布到 FriendlyChat 聊天 Feed 的消息存储在 Cloud Firestore 中。我们来看一下为消息使用的数据结构。为此,请在聊天中发布一条内容为“Hello World”的新消息:

11f5a676fbb1a69a

它应显示为:

fe6d1c020d0744cf.png

在 Firebase 控制台中,点击 Build 部分下的 Firestore Database。您应该会看到邮件集合,以及一个包含您撰写的邮件的文档:

442c9c10b5e2b245

如您所见,聊天消息以文档的形式存储在 Cloud Firestore 中,并且 nameprofilePicUrltexttimestamp 属性已添加到 messages 集合中。

添加欢迎辞

第一个 Cloud Functions 函数添加一条消息,欢迎新用户加入聊天。为此,我们可以使用触发器 functions.auth().onCreate,它会在每次用户首次在 Firebase 应用中登录时运行该函数。将 addWelcomeMessages 函数添加到 index.js 文件中:

index.js

// Adds a message that welcomes new users into the chat.
exports.addWelcomeMessages = functions.auth.user().onCreate(async (user) => {
  functions.logger.log('A new user signed in for the first time.');
  const fullName = user.displayName || 'Anonymous';

  // Saves the new welcome message into the database
  // which then displays it in the FriendlyChat clients.
  await admin.firestore().collection('messages').add({
    name: 'Firebase Bot',
    profilePicUrl: '/images/firebase-logo.png', // Firebase logo
    text: `${fullName} signed in for the first time! Welcome!`,
    timestamp: admin.firestore.FieldValue.serverTimestamp(),
  });
  functions.logger.log('Welcome message written to database.');
});

将此函数添加到特殊的 exports 对象是 Node 的一种方式,可让该函数在当前文件之外访问,并且是 Cloud Functions 所必需的。

在上面的函数中,我们会将“Firebase Bot”发布的新欢迎消息添加到聊天消息列表中。我们通过对 Cloud Firestore 中存储聊天消息的 messages 集合使用 add 方法执行此操作。

由于这是一项异步操作,因此我们需要返回 Promise 以指示 Cloud Firestore 何时完成写入,以免 Cloud Functions 函数过早执行。

部署 Cloud Functions 函数

Cloud Functions 只有在您部署后才会生效。为此,请在命令行中运行以下命令:

firebase deploy --only functions

您应该会看到以下控制台输出:

i  deploying functions
i  functions: ensuring necessary APIs are enabled...
⚠  functions: missing necessary APIs. Enabling now...
i  env: ensuring necessary APIs are enabled...
⚠  env: missing necessary APIs. Enabling now...
i  functions: waiting for APIs to activate...
i  env: waiting for APIs to activate...
✔  env: all necessary APIs are enabled
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (X.XX KB) for uploading
✔  functions: functions folder uploaded successfully
i  starting release process (may take several minutes)...
i  functions: creating function addWelcomeMessages...
✔  functions[addWelcomeMessages]: Successful create operation. 
✔  functions: all functions deployed successfully!

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/friendlypchat-1234/overview

测试函数

函数部署成功后,您需要让用户进行首次登录。

  1. 使用托管网址(格式为 https://<project-id>.firebaseapp.com)在浏览器中打开您的应用。
  2. 让新用户使用 Sign In 按钮在您的应用中首次登录。

262535d1b1223c65

  1. 登录后,系统应该会自动显示欢迎辞:

1c70e0d64b23525b

9. 图片审核

用户可以在聊天中上传各种类型的图片,因此一定要审核令人反感的图片,尤其是在公共社交平台上。在 FriendlyChat 中,发布到聊天的图片会存储在 Google Cloud Storage 中。

借助 Cloud Functions,您可以使用 functions.storage().onFinalize 触发器检测新上传的图片。每次在 Cloud Storage 中上传或修改新文件时,都会运行此测试。

要审核图片,我们将执行以下过程:

  1. 使用 Cloud Vision API 检查图片是否被标记为“成人”或“暴力”。
  2. 如果该映像已被标记,请将其下载到正在运行的 Functions 实例中。
  3. 使用 ImageMagick 对图片进行模糊处理。
  4. 将经过模糊处理的图片上传到 Cloud Storage。

启用 Cloud Vision API

由于我们将在此函数中使用 Google Cloud Vision API,因此您必须在 Firebase 项目上启用此 API。点击此链接,然后选择您的 Firebase 项目并启用此 API:

5c77fee51ec5de49

安装依赖项

为了审核图片,我们将使用 Node.js 版 Google Cloud Vision 客户端库 @google-cloud/vision 通过 Cloud Vision API 运行图片,以检测不当图片。

如需将此软件包安装到您的 Cloud Functions 应用中,请运行以下 npm install --save 命令。请务必从 functions 目录执行此操作。

npm install --save @google-cloud/vision@2.4.0

这将在本地安装软件包,并将它们作为声明的依赖项添加到 package.json 文件中。

导入和配置依赖项

如需导入已安装的依赖项和本部分所需的一些 Node.js 核心模块(pathosfs),请将以下代码行添加到 index.js 文件的顶部:

index.js

const Vision = require('@google-cloud/vision');
const vision = new Vision.ImageAnnotatorClient();
const {promisify} = require('util');
const exec = promisify(require('child_process').exec);

const path = require('path');
const os = require('os');
const fs = require('fs');

由于您的函数将在 Google Cloud 环境中运行,因此您无需配置 Cloud Storage 库和 Cloud Vision 库:它们将自动配置为使用您的项目。

检测不当图片

您将使用 functions.storage.onChange Cloud Functions 触发器,该触发器会在 Cloud Storage 存储分区中创建或修改文件或文件夹后立即运行您的代码。将 blurOffensiveImages 函数添加到 index.js 文件中:

index.js

// Checks if uploaded images are flagged as Adult or Violence and if so blurs them.
exports.blurOffensiveImages = functions.runWith({memory: '2GB'}).storage.object().onFinalize(
    async (object) => {
      const imageUri = `gs://${object.bucket}/${object.name}`;
      // Check the image content using the Cloud Vision API.
      const batchAnnotateImagesResponse = await vision.safeSearchDetection(imageUri);
      const safeSearchResult = batchAnnotateImagesResponse[0].safeSearchAnnotation;
      const Likelihood = Vision.protos.google.cloud.vision.v1.Likelihood;
      if (Likelihood[safeSearchResult.adult] >= Likelihood.LIKELY ||
          Likelihood[safeSearchResult.violence] >= Likelihood.LIKELY) {
        functions.logger.log('The image', object.name, 'has been detected as inappropriate.');
        return blurImage(object.name);
      }
      functions.logger.log('The image', object.name, 'has been detected as OK.');
    });

请注意,我们为将运行该函数的 Cloud Functions 函数实例添加了一些配置。使用 .runWith({memory: '2GB'}) 时,我们要求实例获得 2GB 而不是默认内存,因为此函数属于内存密集型函数。

当该函数被触发时,系统会通过 Cloud Vision API 运行图片,以检测它是否被标记为成人内容或暴力内容。如果根据这些条件检测到图片不合适,我们将对图片进行模糊处理,此过程在 blurImage 函数中完成,接下来将进行介绍。

对图片进行模糊处理

index.js 文件中添加以下 blurImage 函数:

index.js

// Blurs the given image located in the given bucket using ImageMagick.
async function blurImage(filePath) {
  const tempLocalFile = path.join(os.tmpdir(), path.basename(filePath));
  const messageId = filePath.split(path.sep)[1];
  const bucket = admin.storage().bucket();

  // Download file from bucket.
  await bucket.file(filePath).download({destination: tempLocalFile});
  functions.logger.log('Image has been downloaded to', tempLocalFile);
  // Blur the image using ImageMagick.
  await exec(`convert "${tempLocalFile}" -channel RGBA -blur 0x24 "${tempLocalFile}"`);
  functions.logger.log('Image has been blurred');
  // Uploading the Blurred image back into the bucket.
  await bucket.upload(tempLocalFile, {destination: filePath});
  functions.logger.log('Blurred image has been uploaded to', filePath);
  // Deleting the local file to free up disk space.
  fs.unlinkSync(tempLocalFile);
  functions.logger.log('Deleted local file.');
  // Indicate that the message has been moderated.
  await admin.firestore().collection('messages').doc(messageId).update({moderated: true});
  functions.logger.log('Marked the image as moderated in the database.');
}

在上述函数中,从 Cloud Storage 下载图片二进制文件。然后,使用 ImageMagick 的 convert 工具对图片进行模糊处理,并将模糊处理后的版本重新上传到存储分区。接下来,我们删除 Cloud Functions 实例上的文件以释放一些磁盘空间,这是因为同一个 Cloud Functions 实例可能会被重复使用,并且如果文件不清理,磁盘空间可能会用尽。最后,我们为聊天消息添加一个布尔值,表示图片已经过审核,这会触发在客户端上刷新消息。

部署函数

该函数仅在部署后才有效。在命令行中,运行 firebase deploy --only functions

firebase deploy --only functions

您应该会看到以下控制台输出:

i  deploying functions
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (X.XX KB) for uploading
✔  functions: functions folder uploaded successfully
i  starting release process (may take several minutes)...
i  functions: updating function addWelcomeMessages...
i  functions: creating function blurOffensiveImages...
✔  functions[addWelcomeMessages]: Successful update operation.
✔  functions[blurOffensiveImages]: Successful create operation.
✔  functions: all functions deployed successfully!

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview

测试函数

函数部署成功后:

  1. 使用托管网址(格式为 https://<project-id>.firebaseapp.com)在浏览器中打开您的应用。
  2. 登录应用后,上传图片:4db9fdab56703e4a
  3. 请选择最有攻击性的图片进行上传(或者,您也可以使用以下肉食僵尸!),片刻之后,您的帖子应该就会刷新,显示模糊处理后的图片:83dd904fbaf97d2b

10. 新消息通知

在本部分中,您将添加一个 Cloud Functions 函数,用于在有新消息发布时向聊天参与者发送通知。

利用 Firebase Cloud Messaging (FCM),您可以跨平台向用户可靠地发送通知。如需向用户发送通知,您需要拥有用户的 FCM 设备令牌。我们正在使用的聊天 Web 应用已经在用户通过新浏览器或设备首次打开该应用时收集设备令牌。这些令牌存储在 Cloud Firestore 的 fcmTokens 集合中。

如果您想要了解如何在 Web 应用上获取 FCM 设备令牌,则可以参阅 Firebase Web Codelab

发送通知

为了检测新消息的发布时间,您将使用 functions.firestore.document().onCreate Cloud Functions 触发器,该触发器会在 Cloud Firestore 的指定路径上创建新对象时运行您的代码。将 sendNotifications 函数添加到 index.js 文件中:

index.js

// Sends a notifications to all users when a new message is posted.
exports.sendNotifications = functions.firestore.document('messages/{messageId}').onCreate(
  async (snapshot) => {
    // Notification details.
    const text = snapshot.data().text;
    const payload = {
      notification: {
        title: `${snapshot.data().name} posted ${text ? 'a message' : 'an image'}`,
        body: text ? (text.length <= 100 ? text : text.substring(0, 97) + '...') : '',
        icon: snapshot.data().profilePicUrl || '/images/profile_placeholder.png',
        click_action: `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com`,
      }
    };

    // Get the list of device tokens.
    const allTokens = await admin.firestore().collection('fcmTokens').get();
    const tokens = [];
    allTokens.forEach((tokenDoc) => {
      tokens.push(tokenDoc.id);
    });

    if (tokens.length > 0) {
      // Send notifications to all tokens.
      const response = await admin.messaging().sendToDevice(tokens, payload);
      await cleanupTokens(response, tokens);
      functions.logger.log('Notifications have been sent and tokens cleaned up.');
    }
  });

在上面的函数中,我们将从 Cloud Firestore 数据库收集所有用户的设备令牌,并使用 admin.messaging().sendToDevice 函数向每个用户设备令牌发送通知。

清理令牌

最后,我们希望移除不再有效的令牌。当浏览器或设备不再使用我们曾经从用户处获取的令牌时,就会发生这种情况。例如,如果用户撤消了浏览器会话的通知权限,就会发生这种情况。为此,请在 index.js 文件中添加以下 cleanupTokens 函数:

index.js

// Cleans up the tokens that are no longer valid.
function cleanupTokens(response, tokens) {
 // For each notification we check if there was an error.
 const tokensDelete = [];
 response.results.forEach((result, index) => {
   const error = result.error;
   if (error) {
     functions.logger.error('Failure sending notification to', tokens[index], error);
     // Cleanup the tokens that are not registered anymore.
     if (error.code === 'messaging/invalid-registration-token' ||
         error.code === 'messaging/registration-token-not-registered') {
       const deleteTask = admin.firestore().collection('fcmTokens').doc(tokens[index]).delete();
       tokensDelete.push(deleteTask);
     }
   }
 });
 return Promise.all(tokensDelete);
}

部署函数

该函数只有在您部署后才有效。要部署该函数,请在命令行中运行以下命令:

firebase deploy --only functions

您应该会看到以下控制台输出:

i  deploying functions
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (X.XX KB) for uploading
✔  functions: functions folder uploaded successfully
i  starting release process (may take several minutes)...
i  functions: updating function addWelcomeMessages...
i  functions: updating function blurOffensiveImages...
i  functions: creating function sendNotifications...
✔  functions[addWelcomeMessages]: Successful update operation.
✔  functions[blurOffensiveImages]: Successful updating operation.
✔  functions[sendNotifications]: Successful create operation.
✔  functions: all functions deployed successfully!

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview

测试函数

  1. 函数部署成功后,使用托管网址(采用 https://<project-id>.firebaseapp.com 格式)在浏览器中打开您的应用。
  2. 如果您是首次登录该应用,请确保在系统提示时允许显示通知:8b9d0c66dc36153d
  3. 关闭聊天应用标签页或显示其他标签页:仅当该应用在后台运行时,系统才会显示通知。如果您想了解如何在应用处于前台运行时接收消息,请参阅我们的文档
  4. 使用其他浏览器(或无痕式窗口)登录应用并发布消息。您应该会看到第一个浏览器显示的通知:45282ab12b28b926

11. 恭喜!

您已使用 Firebase SDK for Cloud Functions,并向聊天应用添加了服务器端组件。

所学内容

  • 使用 Firebase SDK for Cloud Functions 编写 Cloud Functions 函数。
  • 根据 Auth、Cloud Storage 和 Cloud Firestore 事件触发 Cloud Functions 函数。
  • 为您的 Web 应用添加 Firebase Cloud Messaging 支持。
  • 使用 Firebase CLI 部署 Cloud Functions 函数。

后续步骤

了解详情