我们在玩手机时,某个App收到通知时,会从后台推送到手机上,提醒用户注意,如下所示,
上面是安卓系统的推送效果,那么作为前端开发人员,当用户不在当前页面上或者浏览器最小化时,能不能也接到推送通知呢?
实际上 HTML5 提供通知的 API,它就是 Web Notification,下面跟着笔者看看如何使用它
Web Notification
Web Notification(网页通知)是一种允许网站向用户发送即时通知的技术,即使用户没有在页面上活动。这些通知可以出现在桌面、手机屏幕或浏览器通知区域,用户可以直接看到,并与通知互动。
浏览器提供 Notification类,需要创建它的实例进行使用,Notification
类的三个状态对应于用户对网页通知权限的设置。这些状态用于指示浏览器中用户的通知授权状态。具体来说,它们分别是:
default
这是默认状态,表示用户还没有做出选择,浏览器尚未请求或用户尚未明确授予或拒绝通知权限。granted
表示用户已经授权允许显示通知。用户在浏览器弹出的权限请求框中点击了 "允许" 或 "接受"。denied
表示用户拒绝了通知权限,浏览器在权限请求框中点击了 "拒绝" 或 "不接受"。
所以我们在使用 Notification 时,需要先申请使用权限,只有用户开启后,方可正常使用
申请Notification权限
由于较高版本的浏览器才支持Notification API,所以在使用时需要判断浏览器版本是否支持
if("Notification" in window) {
// do something
}
然后向用户申请使用权限
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
console.log("User granted notification permission");
// do something
} else {
console.log("User denied notification permission");
}
});
而开启通知的代码如下
const notification = new Notification(title, opts);
Notification 属性和方法
Notification
类是用于在网页中显示通知的 JavaScript 接口,它有几个重要的属性和方法。下面是 Notification
类的所有主要属性和方法:
1. 属性
(1) title
- 类型:
string
- 描述:表示通知的标题。通知对象的标题是通过构造函数传递的第一个参数进行设置的。例如:
new Notification('Title', {...})
。
(2) body
- 类型:
string
- 描述:表示通知的主体内容或文本。通知的正文通过构造函数中的
body
属性设置。例如:new Notification('Title', {body: 'Hello, World!'})
。
(3) icon
- 类型:
string
(URL) - 描述:表示通知的图标。它是一个 URL 字符串,用于指定显示在通知上的图标。默认情况下没有图标,浏览器会使用默认的图标。
- 示例:
icon: 'https://example.com/icon.png'
(4) tag
- 类型:
string
- 描述:表示通知的标签。它是一个可选的字符串,用于标识通知。当多个通知具有相同的标签时,后一个通知会替换前一个通知。
- 示例:
tag: 'important-update'
(5) timestamp
- 类型:
number
- 描述:表示通知的创建时间(以毫秒为单位的时间戳)。这个属性由浏览器自动生成。
(6) actions
-
类型:
NotificationAction[]
(可选) -
描述:表示一个通知的动作(如按钮)。它是一个数组,每个元素是一个
NotificationAction
对象,代表通知中的一个交互按钮。 -
NotificationAction
属性:action
:按钮的标识符。title
:按钮的文本。icon
(可选):按钮的图标。
(7) vibrate
- 类型:
Array<number>
(可选) - 描述:设置设备的振动模式。它是一个数组,表示振动的时长(以毫秒为单位)。例如,
[100, 200, 100]
会让设备振动 100 毫秒,暂停 200 毫秒,再振动 100 毫秒。
2. 方法
(1) close()
- 描述:立即关闭通知。调用
close()
方法会使通知消失,无论它是否已经显示完毕。 - 示例:
notification.close()
(2) onclick
-
类型:
function
(事件处理函数) -
描述:该属性设置或返回一个事件处理函数,该函数会在用户点击通知时触发。
-
示例:
notification.onclick = function(event) { window.open('https://example.com'); }
(3) onshow
-
类型:
function
(事件处理函数) -
描述:该属性设置或返回一个事件处理函数,该函数会在通知显示时触发。
-
示例:
notification.onshow = function() { console.log('通知已显示'); }
(4) onclose
-
类型:
function
(事件处理函数) -
描述:该属性设置或返回一个事件处理函数,该函数会在通知被关闭时触发。
-
示例:
notification.onclose = function() { console.log('通知已关闭'); }
(5) onerror
-
类型:
function
(事件处理函数) -
描述:该属性设置或返回一个事件处理函数,该函数会在通知的显示过程中发生错误时触发(例如,无法加载图标或显示通知)。
-
示例:
notification.onerror = function() { console.error('通知显示错误'); }
3. 通知与浏览器交互
除了 Notification
对象的属性和方法外,你还可以结合其他 Web API 来增强通知的交互性。比如,使用 Service Workers 来推送通知,或在通知中添加按钮(通过 actions
)来与用户互动。
总结
Notification
属性:
title
: 通知标题body
: 通知正文icon
: 通知图标tag
: 通知标签timestamp
: 通知时间戳actions
: 通知按钮vibrate
: 振动模式
Notification
方法:
close()
: 关闭通知onclick
: 用户点击通知时触发的事件onshow
: 通知显示时触发的事件onclose
: 通知关闭时触发的事件onerror
: 通知显示错误时触发的事件
使用这些属性和方法,你可以灵活地创建和管理网页通知,并与用户进行交互。
与 React 集成
在了解清楚 Notification 的所有属性和方法后,我们可以将其封装成一个自定义 Hook,以便复用
我们先来确定如何使用,笔者是这样设计的
const { onNotify, onClose } = useNotification(title, opts)
这个 hook 的名字叫 useNotification, 接收两个参数,第一个参数是 title, 第二个参数是一个对象,里面包含很多属性配置和方法, 对外暴露onNotify和onClose两个方法,根据如上的用法,我们来实现useNotification内部
const useNotification = (title, opts) => {
const onNotify = () => {
if (isSupportedNotification) {
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
console.log("User granted notification permission");
// do something
} else {
console.log("User denied notification permission");
}
});
}
};
const onClose = () => {};
return {
onNotify,
onClose,
};
};
这是根据用法和上一章节提到的申请用户权限后的代码,在此基础上继续完善
浏览器是否支持 Notification 使用需要使用 useState 处理
const [isSupportedNotification, setIsSupportedNotification] = useState(false);
useEffect(() => {
setIsSupportedNotification("Notification" in window);
}, []);
向用户申请通知权限后,创建 Notification 实例
notification = new Notification(title, opts);
接收传入的 title 和 opts 参数。接下来就是处理事件。当用户点击通知时
if (typeof opts?.onClick === "function") {
notification.addEventListener("click", (event) => {
event.preventDefault();
opts?.onClick?.();
notification.current.close();
});
}
由于 notification 要在其他方法中使用,推荐使用 useRef 处理
let notification = useRef(null);
其他的方法处理如下
if (isFunction(opts?.onClose)) {
notification.current.onclose = opts?.onClose;
}
if (isFunction(opts?.onError)) {
notification.current.onerror = opts?.onError;
}
if (isFunction(opts?.onShow)) {
notification.current.onshow = opts?.onShow;
}
当用户不对通知做任何操作时,默认 5s后通知消失, 需要加上这么一段逻辑
setTimeout(function () {
notification.current?.close();
}, 5000);
完整的 onNotify 如下所示
const onNotify = () => {
if (isSupportedNotification && inBackground) {
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
console.log("User granted notification permission");
if (timer) {
clearTimeout(timer);
}
if (notification.current) {
notification.current.close();
}
notification.current = new Notification(title, opts);
if (typeof opts?.onClick === "function") {
notification.current.addEventListener("click", (event) => {
event.preventDefault();
opts?.onClick?.();
notification.current.close();
});
}
if (isFunction(opts?.onClose)) {
notification.current.onclose = opts?.onClose;
}
if (isFunction(opts?.onError)) {
notification.current.onerror = opts?.onError;
}
if (isFunction(opts?.onShow)) {
notification.current.onshow = opts?.onShow;
}
timer = setTimeout(function () {
notification.current?.close();
}, timeout);
} else {
console.log("User denied notification permission");
}
});
}
};
其中,inBackground
是为了控制场景下需要启用通知,如果用户在当前页面,可以不用通知,当用户不在当前页面或者浏览器最小化,需要使用通知
完整的 onClose 方法实现如下所示
const onClose = () => {
notification.current?.close();
};
完整的 useNotification 如下所示
import { useState, useEffect, useRef } from "react";
const isFunction = (fn) => typeof fn === "function";
const useNotification = (title, opts) => {
const [isSupportedNotification, setIsSupportedNotification] = useState(false);
const timeout = typeof opts.timeout === "number" ? opts.timeout : 5000;
let notification = useRef(null);
let timer;
const inBackground = opts.inBackground || false;
const onNotify = () => {
if (isSupportedNotification && inBackground) {
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
console.log("User granted notification permission");
if (timer) {
clearTimeout(timer);
}
if (notification.current) {
notification.current.close();
}
notification.current = new Notification(title, opts);
if (typeof opts?.onClick === "function") {
notification.current.addEventListener("click", (event) => {
event.preventDefault();
opts?.onClick?.();
notification.current.close();
});
}
if (isFunction(opts?.onClose)) {
notification.current.onclose = opts?.onClose;
}
if (isFunction(opts?.onError)) {
notification.current.onerror = opts?.onError;
}
if (isFunction(opts?.onShow)) {
notification.current.onshow = opts?.onShow;
}
timer = setTimeout(function () {
notification.current?.close();
}, timeout);
} else {
console.log("User denied notification permission");
}
});
}
};
const onClose = () => {
notification.current?.close();
};
useEffect(() => {
setIsSupportedNotification("Notification" in window);
}, []);
return {
onNotify,
onClose,
};
};
export default useNotification;
由于 React Hook 只能在组件中使用,而实际项目中可能不在组件中使用,所以需要对此优化一下
useNotification 与 zustand 集成
为了既能在组件中使用 useNotification,也能在非组件中使用useNotification,笔者基于 zustand,重新封装了useNotificationStore 方法, 完整的实现如下所示
import { create } from "zustand";
const isFunction = (fn) => typeof fn === "function";
// 只要不在前台就启动通知
const useNotificationStore = (title, opts) =>
create(() => {
const isSupportedNotification = "Notification" in window;
let notification, timer;
const timeout = typeof opts.timeout === "number" ? opts.timeout : 5000;
const onNotify = () => {
if (isSupportedNotification && opts?.inBackground) {
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
console.log("User granted notification permission");
if (notification) {
notification.close();
}
if (timer) {
clearTimeout(timer);
}
notification = new Notification(title, opts);
if (typeof opts?.onClick === "function") {
notification.addEventListener("click", (event) => {
event.preventDefault();
opts?.onClick?.();
notification.close();
});
}
if (isFunction(opts?.onClose)) {
notification.onclose = opts?.onClose;
}
if (isFunction(opts?.onError)) {
notification.onerror = opts?.onError;
}
if (isFunction(opts?.onShow)) {
notification.onshow = opts?.onShow;
}
timer = setTimeout(function () {
notification?.close();
}, timeout);
} else {
console.log("User denied notification permission");
}
});
}
};
const onClose = () => {
notification?.close();
};
return {
onClose,
onNotify,
};
});
export default useNotificationStore;