您知道那些在右上角(Mac)或右下角(Windows)弹出的小通知窗口吗?例如,当您最喜欢的博客发布了新文章或 YouTube 上上传了新视频时,就会弹出这些窗口。这些就是推送通知。
这些通知的魔力在于,它们即使在我们当前不在该网站上时也能显示相关信息(在您批准后)。在支持的移动设备上,您甚至可以关闭浏览器,仍然可以收到通知。
文章系列
- 设置和 Firebase(您当前所在位置!)
- 后端

通知包含浏览器徽标,以便用户知道通知来自哪个软件,以及标题、发送通知的网站 URL、简短描述和自定义图标。
我们将探讨如何实现推送通知。由于它依赖于 Service Workers,如果您不熟悉它或 Push API 的一般功能,请查看以下入门点
我们将要创建什么

为了测试我们的通知系统,我们将创建一个页面,其中包含
- 一个订阅按钮
- 一个添加帖子的表单
- 所有先前发布帖子的列表
可以在 此处 找到包含完整代码的 Github 仓库以及项目的预览
以及工作演示视频
收集所有工具
您可以自由选择最适合您的后端系统。我选择了 Firebase,因为它提供了一个特殊的 API,使实现推送通知服务相对容易。
我们需要
- 一个 Service Worker(因此需要 HTTPS)
- 一个清单文件
- Firebase 实时数据库 来存储我们的帖子
- Firebase Cloud Messaging 允许我们“免费可靠地传递消息”
- Firebase Cloud Functions 监控数据库中的更改并发送通知
在本部分中,我们只关注前端,包括 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)
}
})
}
取消订阅用户
如果用户在再次订阅后点击按钮,则其令牌将被删除。我们重置我们的userToken
和isSubscribed
变量,以及从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"
}
现在我们在前端都设置好了。剩下的就是在下一篇文章中创建我们实际的数据库和监视数据库更改的函数。
文章系列
- 设置和 Firebase(您当前所在位置!)
- 后端
仅出于好奇,在“在网站上显示消息”部分,这种方法在 Safari 和 Edge 中是否也可行?我们可以用它在 Web 应用中创建“消息体验”吗?我阅读的大多数教程都依赖于 Chrome 的推送通知功能,但我们如何在其他浏览器中提供网站内消息?
不可以。请查看 caniuse 中的“推送 API”。
推送通知并非特定于 Chrome。它们在 Firefox、Samsung Internet 和 Opera 中也能正常工作。
浏览器首先需要支持 Service Workers,而 Edge 和 Safari 仅在其预览版中支持。
Safari 也有自己的非标准推送 API 实现。您可以使用它。
这正是我要找的。第二部分什么时候发布?
很棒,但是有没有 Firebase 的替代方案?或者,是否有可能拥有自托管解决方案?
是的,您可以将数据(令牌)发送到您的后端并在自建数据库中存储。
你好朋友。我已经克隆了 Github 仓库,但它与这里展示的示例非常不同,我想知道如何使其工作。你打算什么时候发布第二部分?
嗨,Jonathan,
仓库与这里的示例有何不同?
你写这封邮件时,第二部分已经上线了。