浏览器通知Notification使用全解析,看这篇就够了

340 阅读7分钟

我们在玩手机时,某个App收到通知时,会从后台推送到手机上,提醒用户注意,如下所示,

3db949f5714372bdbbd692e0ae9df693.jpg

上面是安卓系统的推送效果,那么作为前端开发人员,当用户不在当前页面上或者浏览器最小化时,能不能也接到推送通知呢?

实际上 HTML5 提供通知的 API,它就是 Web Notification,下面跟着笔者看看如何使用它

image.png

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;