实现推送通知:设置和 Firebase

Avatar of Pascal Klau (@pascalaoms)
Pascal Klau (@pascalaoms)

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

您知道那些在右上角(Mac)或右下角(Windows)弹出的小通知窗口吗?例如,当您最喜欢的博客发布了新文章或 YouTube 上上传了新视频时,就会弹出这些窗口。这些就是推送通知

这些通知的魔力在于,它们即使在我们当前不在该网站上时也能显示相关信息(在您批准后)。在支持的移动设备上,您甚至可以关闭浏览器,仍然可以收到通知。

文章系列

  1. 设置和 Firebase(您当前所在位置!)
  2. 后端
Notification on Mac via Chrome
Chrome 浏览器在 Mac 上的推送通知

通知包含浏览器徽标,以便用户知道通知来自哪个软件,以及标题、发送通知的网站 URL、简短描述和自定义图标。

我们将探讨如何实现推送通知。由于它依赖于 Service Workers,如果您不熟悉它或 Push API 的一般功能,请查看以下入门点

我们将要创建什么

我们的推送通知演示网站预览

为了测试我们的通知系统,我们将创建一个页面,其中包含

  • 一个订阅按钮
  • 一个添加帖子的表单
  • 所有先前发布帖子的列表

可以在 此处 找到包含完整代码的 Github 仓库以及项目的预览

查看演示站点

以及工作演示视频

收集所有工具

您可以自由选择最适合您的后端系统。我选择了 Firebase,因为它提供了一个特殊的 API,使实现推送通知服务相对容易。

我们需要

在本部分中,我们只关注前端,包括 Service Worker 和清单文件,但要使用 Firebase,您还需要 注册 并创建一个新项目。

实现订阅逻辑

HTML

我们有一个订阅按钮,它在 if 'serviceWorker' in navigator 时启用。在它下面,是一个简单的表单和一个帖子列表。

<button id="push-button" disabled>Subscribe</button>

<form action="#">
  <input id="input-title">
  <label for="input-title">Post Title</label>
  <button type="submit" id="add-post">Add Post</button>
</form>

<ul id="list"></ul>

实现 Firebase

要使用 Firebase,我们需要实现一些脚本。

<script src="https://www.gstatic.com/firebasejs/4.1.3/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.1.3/firebase-database.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.1.3/firebase-messaging.js"></script>

现在,我们可以使用在项目设置 → 常规下提供的凭据初始化 Firebase。发送者 ID 可以在项目设置 → 云消息传递下找到。设置隐藏在左上角的齿轮图标后面。

firebase.initializeApp({
    apiKey: '<API KEY>',
    authDomain: '<PROJECT ID>.firebaseapp.com',
    databaseURL: 'https://<PROJECT ID>.firebaseio.com',
    projectId: '<PROJECT ID>',
    storageBucket: '<PROJECT ID>.appspot.com',
    messagingSenderId: '<SENDER ID>'
})

Service Worker 注册

Firebase 通过创建一个名为 `firebase-messaging-sw.js` 的文件来提供自己的 Service Worker 设置,该文件包含处理推送通知的所有功能。但通常,您需要 Service Worker 执行的操作不仅仅是这些。因此,使用 useServiceWorker 方法,我们可以告诉 Firebase 也使用我们自己的 `service-worker.js` 文件。

现在,我们可以创建一个 userToken 和一个 isSubscribed 变量,稍后将使用它们。

const messaging = firebase.messaging(),
      database  = firebase.database(),
      pushBtn   = document.getElementById('push-button')

let userToken    = null,
    isSubscribed = false

window.addEventListener('load', () => {

    if ('serviceWorker' in navigator) {

        navigator.serviceWorker.register('/service-worker.js')
            .then(registration => {

                messaging.useServiceWorker(registration)

                initializePush()
            })
            .catch(err => console.log('Service Worker Error', err))

    } else {
        pushBtn.textContent = 'Push not supported.'
    }

})

初始化推送设置

请注意 Service Worker 注册后的 initializePush() 函数。它通过在 localStorage 中查找令牌来检查当前用户是否已订阅。如果存在令牌,它会更改按钮文本并将令牌保存在变量中。

function initializePush() {

    userToken = localStorage.getItem('pushToken')

    isSubscribed = userToken !== null
    updateBtn()

    pushBtn.addEventListener('click', () => {
        pushBtn.disabled = true

        if (isSubscribed) return unsubscribeUser()

        return subscribeUser()
    })
}

在这里,我们还处理订阅按钮上的点击事件。我们在点击时禁用按钮,以避免多次触发。

更新订阅按钮

为了反映当前的订阅状态,我们需要调整按钮的文本和样式。我们还可以检查用户在系统提示时是否允许推送通知。

function updateBtn() {

    if (Notification.permission === 'denied') {
        pushBtn.textContent = 'Subscription blocked'
        return
    }

    pushBtn.textContent = isSubscribed ? 'Unsubscribe' : 'Subscribe'
    pushBtn.disabled = false
}

订阅用户

假设用户第一次在现代浏览器中访问我们的网站,因此他尚未订阅。此外,Service Workers 和 Push API确实受支持。当他点击按钮时,会触发 subscribeUser() 函数。

function subscribeUser() {

    messaging.requestPermission()
        .then(() => messaging.getToken())
        .then(token => {

            updateSubscriptionOnServer(token)
            isSubscribed = true
            userToken = token
            localStorage.setItem('pushToken', token)
            updateBtn()
        })
        .catch(err => console.log('Denied', err))

}

在这里,我们通过编写 messaging.requestPermission() 来请求向用户发送推送通知的权限。

浏览器请求发送推送通知的权限。

如果用户阻止此请求,则会按照我们在 updateBtn() 函数中实现的方式调整按钮。如果用户允许此请求,则会生成一个新令牌,并将其保存在变量中以及 localStorage 中。令牌将通过 updateSubscriptionOnServer() 保存到我们的数据库中。

将订阅保存到我们的数据库

如果用户已订阅,我们会定位我们保存令牌的正确数据库引用(在本例中为 device_ids),查找用户之前提供的令牌,并将其删除。

否则,我们希望保存令牌。使用.once('value'),我们接收键值并可以检查令牌是否已存在。这作为对initializePush()localStorage查找的第二层保护,因为令牌可能因各种原因从那里被删除。我们不希望用户收到内容相同的多条通知。

function updateSubscriptionOnServer(token) {

    if (isSubscribed) {
        return database.ref('device_ids')
                .equalTo(token)
                .on('child_added', snapshot => snapshot.ref.remove())
    }

    database.ref('device_ids').once('value')
        .then(snapshots => {
            let deviceExists = false

            snapshots.forEach(childSnapshot => {
                if (childSnapshot.val() === token) {
                    deviceExists = true
                    return console.log('Device already registered.');
                }

            })

            if (!deviceExists) {
                console.log('Device subscribed');
                return database.ref('device_ids').push(token)
            }
        })
}

取消订阅用户

如果用户在再次订阅后点击按钮,则其令牌将被删除。我们重置我们的userTokenisSubscribed变量,以及从localStorage中删除令牌,并再次更新我们的按钮。

function unsubscribeUser() {

    messaging.deleteToken(userToken)
        .then(() => {
            updateSubscriptionOnServer(userToken)
            isSubscribed = false
            userToken = null
            localStorage.removeItem('pushToken')
            updateBtn()
        })
        .catch(err => console.log('Error unsubscribing', err))
}

为了让 Service Worker 知道我们使用 Firebase,我们在任何其他内容之前将脚本导入到 `service-worker.js` 中。

importScripts('https://www.gstatic.com/firebasejs/4.1.3/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/4.1.3/firebase-database.js')
importScripts('https://www.gstatic.com/firebasejs/4.1.3/firebase-messaging.js')

我们需要再次初始化 Firebase,因为 Service Worker 无法访问我们 `main.js` 文件中的数据。

firebase.initializeApp({
    apiKey: "<API KEY>",
    authDomain: "<PROJECT ID>.firebaseapp.com",
    databaseURL: "https://<PROJECT ID>.firebaseio.com",
    projectId: "<PROJECT ID>",
    storageBucket: "<PROJECT ID>.appspot.com",
    messagingSenderId: "<SENDER ID>"
})

在下面,我们添加了围绕处理通知窗口的所有事件。在此示例中,我们关闭通知并在点击通知后打开网站。

self.addEventListener('notificationclick', event => {
    event.notification.close()

    event.waitUntil(
        self.clients.openWindow('https://artofmyself.com')
    )
})

另一个示例是在后台同步数据。阅读Google 的文章了解相关信息。

在网站上显示消息

当我们订阅了新帖子的通知,但同时也在访问博客,并且恰好有新帖子发布时,我们不会收到通知。

解决此问题的一种方法是在网站本身显示不同类型的消息,例如底部的小型 SnackBar。

为了拦截消息的有效负载,我们在 Firebase Messaging 上调用onMessage方法。

此示例中的样式使用了Material Design Lite

<div id="snackbar" class="mdl-js-snackbar mdl-snackbar">
  <div class="mdl-snackbar__text"></div>
  <button class="mdl-snackbar__action" type="button"></button>
</div>
import 'material-design-lite'

messaging.onMessage(payload => {

    const snackbarContainer = document.querySelector('#snackbar')

    let data = {
        message: payload.notification.title,
        timeout: 5000,
        actionHandler() {
            location.reload()
        },
        actionText: 'Reload'
    }
    snackbarContainer.MaterialSnackbar.showSnackbar(data)
})

添加清单文件

本系列这一部分的最后一步是将Google Cloud Messaging 发送者 ID添加到 `manifest.json` 文件中。此 ID 确保 Firebase 允许向我们的应用发送消息。如果您还没有清单文件,请创建一个并添加以下内容。**请勿更改值。**

{
  "gcm_sender_id": "103953800507"
}

现在我们在前端都设置好了。剩下的就是在下一篇文章中创建我们实际的数据库和监视数据库更改的函数。

文章系列

  1. 设置和 Firebase(您当前所在位置!)
  2. 后端