原文地址:notification behaviourhtml
译文地址:通知行为git
译者:任家乐github
到此为止,咱们已经浏览了能够改变通知样式的选项,除了样式,咱们还能够经过一些选项来改变通知的行为。chrome
默认状况下,若是只设置视觉相关选项,调用 showNotification()
会出现如下行为:windows
在这一节中,咱们会探讨如何单独使用一些选项改变默认的通知行为,这相对来讲比较容易实施和利用。promise
当用户点击通知时,默认不会触发任何事件,它并不会关闭或移除通知。浏览器
通知点击事件的常见用法是调用它来关闭通知、同时执行一些其余的逻辑(例如,打开一个窗口或对应用程序进行一些API调用)缓存
为此,咱们须要在 service worker 中添加一个 “notificationclick” 事件监听器。 这个事件将在点击通知时被调用。异步
self.addEventListener('notificationclick', function(event) {
const clickedNotification = event.notification;
clickedNotification.close();
// 点击通知后作些什么
const promiseChain = doSomething();
event.waitUntil(promiseChain);
});
复制代码
正如你在此示例中所看到的,被点击的通知能够经过 event.notification
参数来访问。经过这个参数咱们能够得到通知的属性和方法,所以咱们可以调用通知的 close()
方法,同时执行一些额外的操做。
提示:在程序运行高峰期,你仍然须要调用 event.waitUntil()
保证 service worker 的持续运行。
相比于以前的普通点击行为,actions
的使用能够提供给用户更高级别的交互体验。
在上一节中,咱们知道了如何调用 showNotification()
来定义 actions
:
const title = 'Actions Notification';
const options = {
actions: [
{
action: 'coffee-action',
title: 'Coffee',
icon: '/images/demos/action-1-128x128.png'
},
{
action: 'doughnut-action',
title: 'Doughnut',
icon: '/images/demos/action-2-128x128.png'
},
{
action: 'gramophone-action',
title: 'gramophone',
icon: '/images/demos/action-3-128x128.png'
},
{
action: 'atom-action',
title: 'Atom',
icon: '/images/demos/action-4-128x128.png'
}
]
};
const maxVisibleActions = Notification.maxActions;
if (maxVisibleActions < 4) {
options.body = `This notification will only display ` +
`${maxVisibleActions} actions.`;
} else {
options.body = `This notification can display up to ` +
`${maxVisibleActions} actions.`;
}
registration.showNotification(title, options);
复制代码
若是用户点击了 action 按钮,经过 notificationclick
回调中返回的 event.action
就能够知道被点击的按钮是哪一个。
event.action
会包含全部选项中有关 action
的值的集合。在上面的例子中,event.action
的值则会是: “coffee-action”、 “doughnut-action”、 “gramophone-action” 或 “atom-action” 的其中一个。
所以经过 event.action
,咱们能够检测到通知或 action 的点击,代码以下:
self.addEventListener('notificationclick', function(event) {
if (!event.action) {
// 正常的通知点击事件
console.log('Notification Click.');
return;
}
switch (event.action) {
case 'coffee-action':
console.log('User ❤️️\'s coffee.');
break;
case 'doughnut-action':
console.log('User ❤️️\'s doughnuts.');
break;
case 'gramophone-action':
console.log('User ❤️️\'s music.');
break;
case 'atom-action':
console.log('User ❤️️\'s science.');
break;
default:
console.log(`Unknown action clicked: '${event.action}'`);
break;
}
});
复制代码
tag 选项的本质是一个字符串类型的 ID,以此将通知 “分组” 在一块儿,并提供了一种简单的方法来向用户显示多个通知,这里可能用示例来解释最为简单:
让咱们来展现一个通知,并给它标记一个 tag,例如 “message-group-1”。 咱们能够按照以下代码来展现这个通知:
const title = 'Notification 1 of 3';
const options = {
body: 'With \'tag\' of \'message-group-1\'',
tag: 'message-group-1'
};
registration.showNotification(title, options);
复制代码
这会展现咱们定义好的第一个通知。
咱们再用一个新的 tag “message-group-2” 来标记并展现第二个通知,代码以下:
const title = 'Notification 2 of 3';
const options = {
body: 'With \'tag\' of \'message-group-2\'',
tag: 'message-group-2'
};
registration.showNotification(title, options);
复制代码
这样会展现给用户第二个通知。
如今让咱们展现第三个通知,但不新增 tag,而是重用咱们第一次定义的 tag “message-group-1”。这样操做会关闭以前的第一个通知并将其替换成新定义的通知。
const title = 'Notification 3 of 3';
const options = {
body: 'With \'tag\' of \'message-group-1\'',
tag: 'message-group-1'
};
registration.showNotification(title, options);
复制代码
如今即便咱们连续 3 次调用 showNotification()
也只会展现 2 个通知。
tag
这个选项简单来看就是一个用于信息分组的方式,所以在新通知与已有通知标记为同一个tag时,当前被展现的全部旧通知将会被关闭。
使用 tag
有一个容易被忽略的小细节:当它替换了一个通知时,是没有音效和震动提醒的。
此时 Renotify
选项就有了用武之地。
在写此文时,这个选项大多数应用于移动设备。经过设置它,接收到新的通知时,系统会震动并播放系统音效。
某些场景下,你可能更但愿替换通知时可以提醒到用户,而不是默默地进行。聊天应用则是一个很好的例子。这种状况你须要同时使用 tag
和 Renotify
选项。
const title = 'Notification 2 of 2';
const options = {
tag: 'renotify',
renotify: true
};
registration.showNotification(title, options);
TypeError: Failed to execute 'showNotification' on 'ServiceWorkerRegistration':
Notifications which set the renotify flag must specify a non-empty tag
复制代码
注意: 若是你设置了 Renotify: true
但却没有设置标签,会出现如下报错信息:
类型错误:不可以在 “ServiceWorkerRegistration” 上执行 “showNotification” 方法:设置了 renotify 标识的通知必须声明一个不为空的标签。(TypeError: Failed to execute 'showNotification' on 'ServiceWorkerRegistration':Notifications which set the renotify flag must specify a non-empty tag)
这一选项能够阻止设备震动、音效以及屏幕亮起的默认行为。若是你的通知不须要立马让用户注意到,这个选项是最合适的。
const title = 'Silent Notification';
const options = {
silent: true
};
registration.showNotification(title, options);
复制代码
注意: 若是同时设置了 silent 和 Renotify,silent 选项会取得更高的优先级。
桌面 chrome 浏览器会展现通知一段时间后将其隐藏,而安卓设备的 chrome 浏览器不会有这种行为,通知会一直展现,直到用户对其进行操做。
若是要强制让通知持续展现直到用户对其操做,须要添加 requireInteraction
选项,此选项会展现通知直到用户消除或点击它。
const title = 'Require Interaction Notification';
const options = {
body: 'With "requireInteraction: \'true\'".',
requireInteraction: true
};
registration.showNotification(title, options);
复制代码
请谨慎使用这个选项,由于一直展现通知、并强制让用户停下手头的事情来忽略通知可能会干扰到用户。
在下一节中,咱们会浏览一些 web 上适用的用于管理通知的常见模式,以及如何执行一些常见的 actions
,例如在点击通知时执行打开网页的行为。
译文地址:经常使用的通知模式
译者:任家乐
此篇咱们将会探索 Web 推送的一些经常使用模式,包括使用一些 service worker 提供的 API。
在上一篇中,咱们了解了如何监听 notificationclick
事件。
除了 notificationclick
事件,咱们还能够监听 notificationclose
事件,它会在用户忽略其中一个通知(例如,用户点击了关闭按钮或划掉了通知,而不是点击了它)时被调用。
这个事件一般被用做数据分析,以此追踪用户与通知的互动状况。
self.addEventListener('notificationclose', function(event) {
const dismissedNotification = event.notification;
const promiseChain = notificationCloseAnalytics();
event.waitUntil(promiseChain);
});
复制代码
当收到推送的信息时,一般只须要获取用户点击后的有用数据。例如,获取用户点击通知时打开的页面地址。
若是须要将推送事件中获取的数据传递给通知,最简单的方式就是在调用 showNotification()
时,给参数 options 对象添加一个 data
属性,其值为对象类型,例如如下所示:
const options = {
body: 'This notification has data attached to it that is printed ' +
'to the console when it\'s clicked.',
tag: 'data-notification',
data: {
time: new Date(Date.now()).toString(),
message: 'Hello, World!'
}
};
registration.showNotification('Notification with Data', options);
复制代码
在点击事件的回调内,能够经过 event.notification.data
来获取数据,例如:
const notificationData = event.notification.data;
console.log('');
console.log('The data notification had the following parameters:');
Object.keys(notificationData).forEach((key) => {
console.log(` ${key}: ${notificationData[key]}`);
});
console.log('');
复制代码
对一个通知来讲,打开指定地址的窗口/标签页能够说是一种最多见的反馈,这个咱们能够经过 clients.openWindow() 来实现。
在 notificationclick
事件中,咱们会运行相似下面的代码来实现以上需求:
const examplePage = '/demos/notification-examples/example-page.html';
const promiseChain = clients.openWindow(examplePage);
event.waitUntil(promiseChain);
复制代码
在下一节中,咱们会看下如何检测用户点击通知后跳转的页面是否已被打开,若是已被打开,咱们能够直接呼起已打开的标签页,而不是打开一个新的标签页。
若是可能,咱们应该呼起一个已打开的窗口,而不是在每次用户点击通知时都打开一个新的窗口。
在咱们探索如何实现以前,值得提醒的是你只可以在与通知同域名的页面实现这个需求。由于咱们只能检测咱们本身站点的页面是否已被打开,这也避免了 开发者看到用户正在浏览的全部站点。
再来看下以前的例子,咱们会对代码稍做调整来检测页面 '/demos/notification-examples/example-page.html' 是否已经被打开。
const urlToOpen = new URL(examplePage, self.location.origin).href;
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
event.waitUntil(promiseChain);
复制代码
让咱们逐步浏览下代码。
首先,咱们将示例中目标页面的地址传递给 URL API。这是我从 Jeff Posnick 那学到的一个巧妙的计策。 调用 new URL()
并传入 location 对象,若是传入的第一个参数是相对地址,则会返回页面的绝对地址(例如,“/” 会变成 “https://站点域名” )。
咱们将地址转成了绝对地址则是为了以后与窗口的地址做对比。
const urlToOpen = new URL(examplePage, self.location.origin).href;
复制代码
以后,咱们会经过调用 matchAll()
获得一系列 WindowClient
对象,包含了当前打开的标签页和窗口。(记住,这些标签页只是你域名下的页面)
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true
})
复制代码
matchAll
方法中传入的 options 对象则告诉浏览器咱们只想获取 “window” 类型的对象(例如,只查看标签页、窗口,不包含 web workers [浏览器的其余工做线程])。 includeUncontrolled
属性表示咱们只能获取没有被当前 service worker 控制的全部标签页(本域下),例如 service worker 正在运行当前代码。通常来讲,在调用 matchAll()
时,你一般会将 includeUncontrolled
设置为 true。
咱们 以promiseChain
(promise 链式调用)的形式捕获返回的 promise 对象,所以以后能够将其传 入event.waitUntil()
方法中以此保持咱们的 service worker 持续工做。
当上一步的 matchAll()
返回的 promise 对象已完成异步操做,咱们就能够开始遍历返回的 window 对象,并将这些对象的 URL 和想要打开的目标 URL 进行对比,若是发现有匹配的,则调用 matchingClient.focus()
方法,它会呼起匹配的窗口,引发用户的注意。
若是没有与之匹配的 URL,咱们则采用上一节的方式新开窗口打开地址。
.then((windowClients) => {
let matchingClient = null;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
if (matchingClient) {
return matchingClient.focus();
} else {
return clients.openWindow(urlToOpen);
}
});
复制代码
注意: 咱们会返回 matchingClient.focus()
、clients.openWindow()
方法执行后返回的 promise 对象, 这样 promise 对象就能够组成咱们的 promise 调用链了。
咱们已经看到,给一个通知添加标签后会致使用同一个标签标识的已有通知被替代。
但经过使用通知相关的 API,你能够更灵活地覆盖展现通知。好比一个聊天应用,开发者可能更但愿用新的通知来展现"你有 2 条未读信息"等相似信息,而不是只展现最新接收到的信息。
你能够利用新的通知,或以其余方式操做当前已有通知,使用 registration.getNotifications()API 可以得到到你 APP 中全部当前展现的通知。
让咱们看看如何使用这个 API 去实现刚说的聊天应用的例子。
在聊天应用中,咱们假设每一个通知都有一些包含用户名的数据。
咱们要作的第一件事就是在全部已打开的通知中找到带有具体用户名的用户。首先调用 registration.getNotifications()
方法,以后遍历其结果检测 notification.data
中是否有具体用户名。
const promiseChain = registration.getNotifications()
.then(notifications => {
let currentNotification;
for(let i = 0; i < notifications.length; i++) {
if (notifications[i].data &&
notifications[i].data.userName === userName) {
currentNotification = notifications[i];
}
}
return currentNotification;
})
复制代码
下一步就是用新的通知来替换上一步中得到的通知。
在这个虚拟的消息应用中,咱们会给新的通知添加一个累计新通知数量的数据,每产生新的通知都会累加这个计数,以此来记录用户收到的新信息的数量。
.then((currentNotification) => {
let notificationTitle;
const options = {
icon: userIcon,
}
if (currentNotification) {
// 咱们有一个已经打开的通知,让咱们利用它来作些什么
const messageCount = currentNotification.data.newMessageCount + 1;
options.body = `You have ${messageCount} new messages from ${userName}.`;
options.data = {
userName: userName,
newMessageCount: messageCount
};
notificationTitle = `New Messages from ${userName}`;
// 记得关闭旧的通知
currentNotification.close();
} else {
options.body = `"${userMessage}"`;
options.data = {
userName: userName,
newMessageCount: 1
};
notificationTitle = `New Message from ${userName}`;
}
return registration.showNotification(
notificationTitle,
options
);
});
复制代码
咱们会累加当前展现的通知的信息数,同时依据这个数据来设置通知的主题和内容信息。若是当前没有展现通知,咱们则会展现一个新的通知,其数据中 newMessageCount
的值为 1。
那么第一条信息的通知会是如下这样:
第二条通知会以这样的方式覆盖已有的通知:
这种方法的好处是,若是你的用户目击了通知一个接着一个的出现,相比于只用最新的信息替代当前的通知内容,消息看起来则更紧密相连。
我一直强调的是,你必须在收到推送信息时展现通知,这个规则在大多数状况下是正确的。只有在一种场景下你不须要展现通知, 那就是用户已经打开了你的站点,而且站点已是呼起的状态。
在你的推送事件中,你能够经过检测目标窗口是否已被打开并呼起,来决定是否须要展现通知。
得到浏览器全部窗口、查询当前已呼起窗口的代码能够参考以下:
function isClientFocused() {
return clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then((windowClients) => {
let clientIsFocused = false;
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
if (windowClient.focused) {
clientIsFocused = true;
break;
}
}
return clientIsFocused;
});
}
复制代码
咱们通常使用 clients.matchAll()
来得到当前浏览器下全部窗口对象, 而后遍历其结果去检查 focused
参数。
在推送事件内,咱们会使用以下方法来决定是否展现通知:
const promiseChain = isClientFocused()
.then((clientIsFocused) => {
if (clientIsFocused) {
console.log('Don\'t need to show a notification.');
return;
}
// 窗口并无被呼起,咱们须要展现通知
return self.registration.showNotification('Had to show a notification.');
});
event.waitUntil(promiseChain);
复制代码
咱们已知,能够在用户正在浏览咱们站点的时候不进行通知。可是,若是你仍然想让用户知道这个推送事件已经发生,但又以为进行通知太过强硬,应该如何处理?
其中一个方法就是利用 service worker 给页面发送消息,这种状况下页面可以给用户展现通知或更新,以此让用户知晓到这个推送事件的发生。固然,只有当用户对于轻量级通知感到更友好时,这种作法才有用。
若是咱们接收到了一个推送,而且检测到了咱们的 APP 已经被打开了,那么咱们就能够"发送消息"给每一个打开的页面,就像如下这样:
const promiseChain = isClientFocused()
.then((clientIsFocused) => {
if (clientIsFocused) {
windowClients.forEach((windowClient) => {
windowClient.postMessage({
message: 'Received a push message.',
time: new Date().toString()
});
});
} else {
return self.registration.showNotification('No focused windows', {
body: 'Had to show a notification instead of messaging each page.'
});
}
});
event.waitUntil(promiseChain);
复制代码
在每一个页面中,咱们经过监听消息(message)事件来接收消息:
navigator.serviceWorker.addEventListener('message', function(event) {
console.log('Received a message from service worker: ', event.data);
});
复制代码
在这个消息监听器中,你能够作任何你想作的事,例如展现自定义的视图,或者彻底忽略这个消息。
值得注意的是,若是你没有在你的网页中定义消息监听器,那么 service worker 推送的消息将不会作任何事。
有一个场景可能超出了这本书的范畴,可是依然值得探讨,那就是缓存你但愿用户点击通知后访问的网页,以此来提高web应用的总体用户体验。
这就须要设置你的 service worker 来处理这些 fetch
事件,但若是你监听了 fetch
事件,请确保在展现你的通知以前,经过缓存你须要的页面和资源来充分利用它在 push
事件中的优点。
想要了解更多缓存相关信息,请参考服务工做线程:简介