八、使用服务工作器发送推送通知

在本章中,我们将完成应用发送推送通知的过程。实施有点复杂;它需要许多移动部件才能工作(根据我的经验,在移动或 web 上的任何推送通知实现都是如此)。令人兴奋的是,我们可以接触到许多新的知识领域,例如设备令牌云功能

在开始之前,让我们花一点时间来概述一下建立推送通知的过程。到目前为止,我们已经启动并运行了消息服务工作器。此服务工作器将坐在那里等待收到新通知。一旦发生这种情况,它将处理与显示该通知有关的所有事情,因此我们不必担心(至少)。

我们要做的是将该消息发送给服务工作器。假设我们的应用有 1000 个用户,每个用户都有一个唯一的设备。每个设备都有一个唯一的令牌,用于将其标识给 Firebase。我们需要跟踪所有这些令牌,因为当我们想要发送通知时,我们需要准确地告诉 Firebase 要将其发送到哪些设备。

所以,这是第一步——为使用我们的应用的设备设置并维护一个包含所有令牌的数据库表。正如我们将看到的,这还必然涉及询问用户是否首先需要通知。

保存令牌后,我们可以告诉 Firebase 在数据库中侦听新消息,然后向所有设备(基于令牌)发送消息详细信息通知。作为一个小的额外复杂性,我们必须确保不向创建消息的用户发送通知。

这个阶段(告诉 Firebase 发送通知)实际上发生在我们的应用之外。它发生在神秘的“云”中,我们将在那里主持一个功能来处理这个过程;稍后再谈。

我们处理这项相当复杂的工程的方法是慢慢地处理,一次一件。确保您仔细遵循代码示例;通知的性质意味着,在实现完成之前,我们无法完全测试我们的实现,因此请尽最大努力避免在实现过程中出现小错误。

在本章中,我们将介绍以下主题:

  • 正在请求显示通知的权限
  • 跟踪和保存用户令牌
  • 使用云函数发送通知

好吧,我们开始吧!

请求许可

正如前面的介绍所解释的,我们在本章中有很多功能要创建。为了将所有内容放在一个地方,而不会弄乱我们的App.js,我们将创建一个单独的 JavaScript 类来管理与通知相关的所有内容。这是我非常喜欢 React 的模式,用于提取未附加到任何一个组件的功能。在我们的src/文件夹中,在components文件夹旁边,让我们创建一个名为resources的文件夹,其中有一个名为NotificationResource.js的文件。

我们班的基本大纲如下:

export default class NotificationResource {

}

我们创建一个 JavaScript 类并将其导出。

For those unfamiliar with JavaScript classes (especially for those familiar with classes in other languages), I encourage you to read the MDN article explaining the basics, at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes.

在我们忘记之前,让我们将其导入我们的App.js中:

import NotificationResource from '../resources/NotificationResource';

当我们的应用启动时,我们希望请求用户允许向他们发送通知。请注意,Firebase 会记住用户是否已经接受或拒绝了我们的请求,因此我们不会每次都用弹出窗口来打扰他们,除非他们以前没有被询问过。

以下是我们将如何处理此过程:

  1. 当我们的应用挂载时,我们将创建一个NotificationResource类的新实例,并将其传递给 Firebase 消息库(我们传入该实例是为了避免我们必须将其导入NotificationResource.js文件,因为我们已经在App.js中访问了它)。
  2. NotificationResource类第一次被实例化时,我们将使用传入的 Firebase 消息传递库立即向用户请求权限。

如果您清楚这些步骤,我鼓励您首先尝试自己实施这些步骤。如果你完全不知道我们会怎么做,别担心,我们会解决的。

好的,让我们从我们的应用componentDidMount开始。这就是我们想要创建NotificationResource实例的地方:

componentDidMount() {
   this.notifications = new NotificationResource();

我们将NotificationResource实例设置为App的属性;这将允许我们访问App.js内的其他位置。

如前所述,我们还希望传入 Firebase 消息传递库:

componentDidMount() {
   this.notifications = new NotificationResource(firebase.messaging());

每个 JavaScript 类都自动有一个constructor方法,在创建实例时调用该方法。这就是我们所说的“新”NotificationResource()。我们放在括号内的任何东西都作为参数传递给构造函数。

让我们跳回到NotificationResource.js并设置:

export default class NotificationResource {
  constructor(messaging) {
    console.log(“Instantiated!”);
  }
}

如果您启动应用,App一安装,您就会在控制台中看到"Instantiated!"

下一步是使用我们的messaging库访问用户以获得向其发送通知的权限:

export default class NotificationResource {
     constructor(messaging) {
       this.messaging = messaging;
 try {
 this.messaging
 .requestPermission()
 .then(res => {
 console.log('Permission granted');
 })
 .catch(err => {
 console.log('no access', err);
 });
 } catch(err) {
 console.log('No notification support.', err);
 }
} } 

我们对App中的NotificationResourcemessaging库做了同样的事情,即将其保存为我们资源的属性,以便我们可以在其他地方使用。然后,我们点击requestPermission功能。

如果我们回到我们的应用,我们会看到:

单击 Allow,您将在控制台中看到授予的权限。

If you've previously built a personal project using localhost:8080 and allowed notifications, you won't see this popup. You can forget your previous preference by clicking on the icon to the left of the URL in the preceding screenshot, and resetting notifications to Ask.

现在我们已经有了开始窃听用户的权限,我们将开始通过令牌跟踪他们的所有设备。

跟踪代币

令牌是用户设备的唯一标识符。它有助于 Firebase 确定向何处发送推送通知。为了正确发送通知,我们需要在数据库中保留所有当前设备令牌的记录,并确保其是最新的。

我们可以通过 Firebasemessaging库访问用户设备的令牌。有两种方法特别有用:onTokenRefreshgetToken。两者都有相当不言自明的名称,因此我们将深入讨论实现:

 export default class NotificationResource {
     constructor(messaging) {
       this.messaging = messaging;
      try {
        this.messaging
          .requestPermission()
          .then(res => {
            console.log('Permission granted');
          })
         .catch(err => {
          console.log('no access', err);
          });
      } catch(err) {
        console.log('No notification support.', err);
      }
};
   this.messaging.getToken().then(res => {
 console.log(res);
 });
 }

当你的应用刷新时,你会看到一个很长的数字和字母字符串。这是您设备的标识。我们需要将其保存到数据库中。

每当令牌改变时,调用firebase.messaging().onTokenRefresh。我们的应用可以删除令牌,或者当用户清除浏览器数据时,会生成一个新的令牌。当这种情况发生时,我们需要覆盖数据库中的旧令牌。关键部分是覆盖;如果我们不删除旧令牌,我们将通过发送到不存在的设备来浪费 Firebase 的时间。

因此,我们有四个步骤来涵盖:

  1. 当令牌更改时,获取新令牌。
  2. 在数据库中查找现有令牌。
  3. 如果存在旧的,则将其更换。
  4. 否则,将新令牌添加到数据库中。

为了完成这张清单,我们需要完成一系列中间任务,但让我们先来看看这个粗略的计划。

我们将在我们的NotificationResource中添加四个功能:setupTokenRefreshsaveTokenToServerfindExistingTokenregisterToken。您可以看到最后两个步骤如何与检查表中的最后两个步骤相一致。

让我们从setupTokenRefresh开始。我们将从构造函数调用它,因为它将负责注册侦听器以进行令牌更改:

   export default class NotificationResource {
     constructor(messaging) {
       this.messaging = messaging;
      try {
        this.messaging
          .requestPermission()
          .then(res => {
            console.log('Permission granted');
          })
         .catch(err => {
          console.log('no access', err);
          });
      } catch(err) {
        console.log('No notification support.', err);
      }
  } 
} 

在我们使用 Firebase 配置了所有“打开”侦听器之后,应该熟悉这种模式。

接下来我们将创建saveTokenToServer,并从setupTokenRefresh调用它:

 setupTokenRefresh() {
   this.messaging.onTokenRefresh(() => {
     this.saveTokenToServer();
   });
 }

 saveTokenToServer() {
   // Get token
   // Look for existing token
   // If it exists, replace
   // Otherwise, create a new one
 }

好的,现在我们可以逐一浏览这些评论。我们已经知道如何获取令牌:

saveTokenToServer() {
   this.messaging.getToken().then(res => {
     // Look for existing token
     // If it exists, replace
     // Otherwise, create a new one
   });
 }

对于下一步,查找现有令牌;我们目前无法访问数据库中保存的以前的令牌(好的,现在还没有,但会有)。

因此,我们需要在数据库中创建一个表来保存令牌。为了方便起见,我们称之为fcmTokens。它还不存在,但只要我们向它发送一些数据,它就会存在。这就是 Firebase 数据的美妙之处——您可以将数据发送到一个不存在的表,然后创建并填充它。

正如我们在App.js中对消息所做的那样,让我们为NotificationResource的构造函数中的/fcmTokens表中的值添加一个侦听器:

export default class NotificationResource {
  allTokens = [];
 tokensLoaded = false;

  constructor(messaging, database) {
    this.database = database;
    this.messaging = messaging;
         try {
        this.messaging
          .requestPermission()
          .then(res => {
            console.log('Permission granted');
          })
         .catch(err => {
          console.log('no access', err);
          });
      } catch(err) {
        console.log('No notification support.', err);
      }};
    this.setupTokenRefresh();
    this.database.ref('/fcmTokens').on('value', snapshot => {
 this.allTokens = snapshot.val();
 this.tokensLoaded = true;
 });
  }

您将注意到,我们现在希望将数据库实例传递到构造函数中。让我们跳回到App.js来设置:

componentDidMount() {
   this.notifications = new NotificationResource(
      firebase.messaging(),
      firebase.database()
    );

好的,这太完美了。

如果您在数据库侦听器内部console.logoutsnapshot.val(),它将为 null,因为我们的/fcmTokens表中没有值。让我们注册一个:

saveTokenToServer() {
   this.messaging.getToken().then(res => {
     if (this.tokensLoaded) {
       const existingToken = this.findExistingToken(res);
       if (existingToken) {
         // Replace existing toke
       } else {
         // Create a new one
       }
     }
   });
 }

如果加载了令牌,我们可以检查现有令牌。如果未加载令牌,则不执行任何操作。这似乎有些奇怪,但我们希望确保不会创建重复的值。

我们如何找到现有的令牌?好的,在我们的构造函数中,我们将在数据库中加载令牌值的结果保存到this.allTokens。我们将简单地对它们进行循环,看看它们是否与由getToken生成的res变量相匹配:

findExistingToken(tokenToSave) {
   for (let tokenKey in this.allTokens) {
     const token = this.allTokens[tokenKey].token;
     if (token === tokenToSave) {
       return tokenKey;
     }
   }
   return false;
 }

此方法的重要部分是,tokenToSave将是一个字符串(前面看到的数字和字母的随机组合),this.allTokens将是从数据库加载的令牌对象的集合,因此是this.allTokens[tokenObject].token业务。

因此,findExistingToken将返回匹配的令牌对象的密钥,或者返回 false。从那里,我们可以更新现有的令牌对象,或者创建一个新的令牌对象。我们将了解为什么在尝试更新令牌时返回密钥(而不是对象本身)很重要。

将用户附加到令牌

在我们继续讨论这两种情况之前,让我们退一步,想想推送通知将如何工作,因为我们需要讨论一个重要的警告。

当用户发送消息时,我们希望通知每个用户,但创建消息的用户除外(这会很恼火),因此我们需要某种方式向数据库中的每个令牌发送通知,但属于发送消息的用户的令牌除外。

我们如何才能防止这种情况?我们如何将用户的消息与用户的令牌相匹配?

我们可以访问消息对象中的用户 ID(也就是说,我们总是将 ID 与消息内容一起保存)。如果我们对令牌做了类似的操作,并将用户 ID 与令牌一起保存,以便确定哪个用户属于哪个设备,该怎么办?

这似乎是一个非常简单的解决方案,但这意味着我们需要访问NotificationResource中当前用户的 ID。让我们马上开始,然后继续编写和更新令牌。

在 NotificationResource 中更改用户

我们在App.js中已经有了处理用户更改的方法——我们的老朋友onAuthStateChanged。让我们连接到它,并使用它调用NotificationResource中的一个方法:

componentDidMount() {
   this.notifications = new NotificationResource(firebase.messaging(), firebase.database());
  firebase.auth().onAuthStateChanged((user) => {
     if (user) {
       this.setState({ user });
       this.listenForMessages();
       this.notifications.changeUser(user);
     } else {
       this.props.history.push('/login')
     }
   });

然后,在NotificationResource内:

changeUser(user) {
   this.user = user;
 }

顺便说一句,这有助于我们解决代币的另一个问题。如前所述,在生成新令牌时调用onTokenRefresh,这可能是因为用户删除了浏览器数据,也可能是因为 web 应用删除了以前的令牌。但是,如果我们将用户 ID 与令牌一起保存,我们需要确保在用户更改时更新该 ID,因此每次用户更改时,我们都必须调用我们的saveTokenToServer方法:

changeUser(user) {
   this.user = user;
   this.saveTokenToServer();
 }

好的,现在我们可以回到saveTokenToServer中的if-else语句,开始保存一些代币。

创建新令牌

让我们从后一种情况开始,创建一个新的令牌。我们将创建一个名为registerToken的新方法,传入getToken调用的结果:

saveTokenToServer() {
   this.messaging.getToken().then(res => {
     if (this.tokensLoaded) {
       const existingToken = this.findExistingToken(res);
       if (existingToken) {
         // Replace existing token
       } else {
         this.registerToken(res);
       }
     }
   });
 }

然后,我们的新方法:

  registerToken(token) {
    firebase
      .database()
      .ref('fcmTokens/')
      .push({
        token: token,
        user_id: this.user.uid
      });
  }

我们保存令牌和用户 ID。完美。

更新现有令牌

我们将对更新令牌执行类似的操作,但这次我们需要访问数据库中的现有令牌。

此处添加一个console.log用于测试目的:

saveTokenToServer() {
   this.messaging.getToken().then(res => {
     if (this.tokensLoaded) {
       const existingToken = this.findExistingToken(res);
       if (existingToken) {
         console.log(existingToken);
       } else {
         this.registerToken(res);
       }
     }
   });
 }

然后,尝试与不同的用户登录和注销应用。每次都应该看到相同的existingToken键:

我们可以使用它从数据库中的fcmToken表中获取现有条目,并对其进行更新:

saveTokenToServer() {
  this.messaging.getToken().then(res => {
    if (this.tokensLoaded) {
      const existingToken = this.findExistingToken(res);
      if (existingToken) {
        firebase
 .database()
 .ref(`/fcmTokens/${existingToken}`)
 .set({
 token: res,
 user_id: this.user.uid
 });
      } else {
        this.registerToken(res);
      }
    }
  });
}

好吧,那太多了。让我们仔细检查一下这是否正常工作。转到console.firebase.com并检查数据库选项卡。尝试使用两个不同的用户再次登录和退出应用。您应该看到匹配的令牌条目每次都更新其用户 ID。然后,尝试登录到其他设备(在执行另一个 firebase 部署后),并看到另一个令牌出现。魔术

现在,我们有一个使用我们的应用的每个设备的令牌表,以及最后与该设备关联的用户 ID。我们现在已经准备好进入推送通知的最佳阶段——实际发送它们。

这是最后的NotificationResource.js

export default class NotificationResource {
  allTokens = [];
  tokensLoaded = false;
  user = null;

  constructor(messaging, database) {
    this.messaging = messaging;
    this.database = database;
          try {
        this.messaging
          .requestPermission()
          .then(res => {
            console.log('Permission granted');
          })
         .catch(err => {
          console.log('no access', err);
          });
      } catch(err) {
        console.log('No notification support.', err);
      };
    this.setupTokenRefresh();
    this.database.ref('/fcmTokens').on('value', snapshot => {
      this.allTokens = snapshot.val();
      this.tokensLoaded = true;
    });
  }

  setupTokenRefresh() {
    this.messaging.onTokenRefresh(() => {
      this.saveTokenToServer();
    });
  }

  saveTokenToServer() {
    this.messaging.getToken().then(res => {
      if (this.tokensLoaded) {
        const existingToken = this.findExistingToken(res);
        if (existingToken) {
          firebase
            .database()
            .ref(`/fcmTokens/${existingToken}`)
            .set({
              token: res,
              user_id: this.user.uid
            });
        } else {
          this.registerToken(res);
        }
      }
    });
  }

  registerToken(token) {
    firebase
      .database()
      .ref('fcmTokens/')
      .push({
        token: token,
        user_id: this.user.uid
      });
  }

  findExistingToken(tokenToSave) {
    for (let tokenKey in this.allTokens) {
      const token = this.allTokens[tokenKey].token;
      if (token === tokenToSave) {
        return tokenKey;
      }
    }
    return false;
  }

  changeUser(user) {
    this.user = user;
    this.saveTokenToServer();
  }
}

发送推送通知

早在本书的开头,当我们初始化 Firebase 时,我们检查了一个函数选项。这在我们的根目录中创建了一个名为functions的文件夹,到目前为止,我们已经忽略了该文件夹(如果您没有此文件夹,可以再次运行firebase init,并确保您在第一个问题中勾选了功能和主机。有关更多信息,请参阅 Firebase 一章)。

functions文件夹允许我们使用 Firebase 云功能。以下是谷歌对它们的定义:

“Cloud Functions gives developers access to Firebase and Google Cloud events, along with scalable computing power to run code in response to those events.”

这是最简单的定义——在应用之外响应事件运行的代码。我们将一些不属于我们应用任何特定实例(因为它涉及我们应用的所有实例)的功能提取到云中,并让 Firebase 自动运行。

让我们敞开心扉开始工作吧。

编写我们的云函数

首先,我们可以初始化我们的应用,如下所示:

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

Cloud functions=响应事件运行的代码,那么我们的事件是什么?

我们希望在创建新消息时通知用户。因此,事件是一条新消息,或者更具体地说,是在数据库的 messages 表中创建一个新条目时。

我们将index.js的导出定义为一个名为sendNotifications的函数,该函数为/messagesonWrite事件定义一个监听器:

exports.sendNotifications = functions.database
  .ref('/messages/{messageId}')
  .onWrite(event => {});

本节中的所有其他内容都将在事件侦听器中进行。

首先,我们从事件中获取快照:

 const snapshot = event.data;

现在,我们不支持编辑消息;但总有一天我们会的。我们不希望在该事件中出现推送通知,因此如果onWrite由更新触发(快照具有以前的值),我们将提前返回:

const snapshot = event.data;
if (snapshot.previous.val()) {
   return;
 }

然后,我们将构造通知。我们定义了一个带有嵌套通知对象的对象,包括一个titlebodyiconclick_action

const payload = {
   notification: {
     title: `${snapshot.val().author}`,
     body: `${snapshot.val().msg}`,
     icon: 'img/icon.png',
     click_action: `https://${functions.config().firebase.authDomain}`
   }
 };

title来自与该消息相关联的用户电子邮件。body就是信息本身。它们都被包装在模板字符串中,以确保它们以字符串的形式出现。这只是一件安全的事情!

然后,我们使用我们的应用图标作为通知图标。注意路径——图标实际上不存在于我们的functions文件夹中,但由于它将被部署到我们应用的根目录中(在build文件夹中),我们可以引用它。

最后,我们的click_action应该将用户带到应用。我们通过配置获取域 URL。

下一步是向相关设备发送有效负载。系好安全带,这将是一段很好的代码。

发送到代币

让我们写下我们需要采取的步骤:

  1. 获取数据库中所有令牌的列表。
  2. 仅筛选该列表中不属于发送消息的用户的令牌。
  3. 将通知发送到设备。
  4. 如果任何设备由于无效或未注册的令牌而无法接收通知,请从数据库中删除其令牌。

最后一步是通过定期从数据库中删除无效令牌来保持整洁。

好的,听起来很有趣。请记住,这些都在onWrite的事件侦听器中。这是第一步:

return admin
      .database()
      .ref('fcmTokens')
      .once('value')
      .then(allTokens => {
        if (allTokens.val()) {

        }
      });

这将使用数据库.once方法获取令牌表的一次性查看。从那里,我们可以继续,如果我们真的有一些代币保存。

为了过滤我们的结果,我们将进行一个非常类似于findExistingToken方法的循环:

.then(allTokens => {
  if (allTokens.val()) {
    const tokens = [];
 for (let fcmTokenKey in allTokens.val()) {
 const fcmToken = allTokens.val()[fcmTokenKey];
 if (fcmToken.user_id !== snapshot.val().user_id) {
 tokens.push(fcmToken.token);
 }
 }
  }
});

我们循环遍历所有令牌,如果user_id与消息的user_id不匹配,我们将其推送到有效令牌数组中。

第三步的时间;向每个设备发送通知,如图所示:

.then(allTokens => {
  if (allTokens.val()) {
    const tokens = [];
    for (let fcmTokenKey in allTokens.val()) {
      const fcmToken = allTokens.val()[fcmTokenKey];
      if (fcmToken.user_id !== snapshot.val().user_id) {
        tokens.push(fcmToken.token);
      }
    }
    if (tokens.length > 0) {
 return admin
 .messaging()
 .sendToDevice(tokens, payload)
 .then(response => {});
 }
  }
});

这很简单。我们传递sendToDevice一组令牌和我们的有效负载对象。

最后,让我们进行清理:

if (tokens.length > 0) {
  return admin
    .messaging()
    .sendToDevice(tokens, payload)
    .then(response => {
      const tokensToRemove = [];
 response.results.forEach((result, index) => {
 const error = result.error;
 if (error) {
 console.error(
 'Failure sending notification to',
 tokens[index],
 error
 );
 if (
 error.code === 'messaging/invalid-registration-token' ||
 error.code ===
 'messaging/registration-token-not-registered'
 ) {
 tokensToRemove.push(
 allTokens.ref.child(tokens[index]).remove()
 );
 }
 }
 });
 return Promise.all(tokensToRemove);
 });
}

这段代码应该很简单,除了返回Promise.all之外。原因是在每个令牌条目上调用remove()返回一个承诺,我们只返回所有此类承诺的解决方案。

以下是最终文件:

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

exports.sendNotifications = functions.database
  .ref('/messages/{messageId}')
  .onWrite(event => {
    const snapshot = event.data;
    if (snapshot.previous.val()) {
      return;
    }
    const payload = {
      notification: {
        title: `${snapshot.val().author}`,
        body: `${snapshot.val().msg}`,
        icon: 'img/icon.png',
        click_action: `https://${functions.config().firebase.authDomain}`
      }
    };
    return admin
      .database()
      .ref('fcmTokens')
      .once('value')
      .then(allTokens => {
        if (allTokens.val()) {
          const tokens = [];
          for (let fcmTokenKey in allTokens.val()) {
            const fcmToken = allTokens.val()[fcmTokenKey];
            if (fcmToken.user_id !== snapshot.val().user_id) {
              tokens.push(fcmToken.token);
            }
          }
          if (tokens.length > 0) {
            return admin
              .messaging()
              .sendToDevice(tokens, payload)
              .then(response => {
                const tokensToRemove = [];
                response.results.forEach((result, index) => {
                  const error = result.error;
                  if (error) {
                    console.error(
                      'Failure sending notification to',
                      tokens[index],
                      error
                    );
                    if (
                      error.code === 'messaging/invalid-registration-token' ||
                      error.code ===
                        'messaging/registration-token-not-registered'
                    ) {
                      tokensToRemove.push(
                        allTokens.ref.child(tokens[index]).remove()
                      );
                    }
                  }
                });
                return Promise.all(tokensToRemove);
              });
          }
        }
      });
  });

测试推送通知

运行**yarn deploy**,然后我们可以测试推送通知。

最简单的测试方法是打开我们部署的应用的一个标签,然后在一个匿名标签中打开另一个版本(使用 Chrome)。使用不同的用户登录到每个选项卡,当您发送消息时,应看到以下内容:

注意,两个选项卡不能同时处于焦点位置;您需要同时打开两个选项卡,但要从其中一个选项卡切换,否则通知将不会显示。

调试推送通知

如果遇到任何问题,可以尝试以下步骤。

检查云功能日志

登录console.firebase.com后,在“功能”选项卡下有一个日志选项卡,显示每个功能的执行情况。任何错误都会显示在这里,我们配置的任何旧令牌删除也会显示在这里。检查以确保 A)发送消息时功能实际正在运行,B)没有干扰发送的错误。

检查服务工作器

如前所述,如果在 Chrome DevTools 应用中选中了 update on reload,则服务工作器应该在其大小存在任何字节差异的情况下进行更新,以及在每次重新加载后进行更新。然而,即使采取了这些步骤,我发现服务工作器通常并没有实际更新重新部署。如果您遇到问题,请单击 DevTools 的“应用服务工作器”选项卡下每个实例旁边的“取消注册”。然后,单击每个服务工作器文件的名称,以确保代码与您的build文件夹中的内容匹配。

检查代币

确保令牌在数据库中正确保存和更新。不应存在具有不同用户 ID 的重复项。

总结

推送通知很棘手。在这一章中,我们必须编写大量代码,同时使用很少的基准进行检入。如果遇到问题,请确保所有代码都与示例匹配。

不过,一旦您的通知生效,我们将在 web 应用和本机应用之间架起一座桥梁。现在,是时候让我们的应用可以由用户安装,从而进入本地应用的世界了。