浏览器通过Notification API提供了消息通知的能力,由浏览器为载体,发送系统通知,可以在不安装APP的情况下进行消息推送,⚠️消息推送需要在HTTPS环境中使用。

熟悉API

这里需要用到两个API,一个用于浏览器触发通知,另一个用于注册Google的推送服务。

Notification

构造函数

new Notification(title[, options])

执行一个构造函数就会发送一条通知,返回一个通知实例用于控制通知的关闭以及设置句柄,如果在ServiceWorker中发送通知需要通过ServiceWorkerRegistration.showNotification方法来发送通知,参数是一样的。

参数比较多,详情可见MDN——Notification,这里列举一些常用的参数。

  • title:消息的标题
  • options:设置通知的自定义内容
    • icon:一个图标URL字符串,用于展示消息图标⚠️图标会有跨域限制
    • body:消息主体内容
    • requireInteraction:布尔值,控制消息是否会自动折叠
new Notification('通知标题', {
    body: '通知内容',
    icon: 'http://localhost:8000/favicon.ico',
  })

上面的代码会显示这样的通知。

image-20240315230656685

静态属性

Permission,只读属性,用于表明当前用于是否授权当前网站发送通知,可能的值有:

  • granted:用户已经授权通知。
  • denied:用户拒绝通知。
  • default:未知,即没有授权也没有拒绝过通知。

静态方法

requestPermission(),用于向用户请求通知授权,返回一个Promise对象,值的内容和静态属性Notification.permission的值一样。

实例方法

close(),用于关闭或移除当前通知,⚠️即使消息被收到托盘中也会被移除,一般用于移除过期的通知,例如用户已经在页面中阅读了消息内容。

事件

click,点击消息时触发

close,关闭消息时触发

error,调用通知出错时触发,用于阻止错误的消息

show,消息显示时触发

PushManager.subscribe

返回一个 Promise 形式的 PushSubscription 对象,该对象包含了推送订阅详情。如果当前 service worker 没有已存在的订阅,则会创建一个新的推送订阅。

PushManager.subscribe(options)

  • userVisibleOnly: 布尔值,表示返回的推送订阅将只能被用于对用户可见的消息。
  • applicationServerKey:推送服务器用来向客户端应用发送消息的公钥。

返回值一个 PushSubscription 的Promise对象,格式如下

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/fsD_4pKPPs4:APA91bHHlK6AupiRhvpnvO01jD_4b-HnSDDbQ0Pz17njO0……",
  "expirationTime": null,
  "keys": {
    "p256dh": "BGLh9okhkFg_KNyoWJ-fdVTThfOeUvda9-pRxXAaCT5nSXjzXu_oxj5isY9v……",
    "auth": "Uld6AooHCyfr75_uYc……"
  }
}

实战练习

实战部分我们来实现一个前端+后端的消息推送,底层利用了Google FCM(Chrome 浏览器内置),后端推送部分利用web-push这个工具库来完成。

前端部分

const PUBLIC_KEY = "和后端一样的公钥";
/** @type PushSubscription */
let subscription;

(async function () {
  const register = await navigator.serviceWorker.register("./worker.js", {
    scope: "/",
  });

  subscription = await register.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: PUBLIC_KEY,
  });
})();

async function send() {
  const value = document.getElementById('message')?.value

  await fetch("/api/notice", {
    method: "POST",
    body: JSON.stringify({ subscription, title: '系统通知', message: value }),
    headers: {
      "Content-Type": "application/json",
    },
  });
}

service worker用于接收消息通知,并弹出消息提醒,这里利用的是push事件。

self.addEventListener('push', function (e) {
  const data = e.data.json();
  self.registration.showNotification(
    data.title,
    {
      body: data.message,
    }
  );
})

后端部分

初始化一个Koa环境,除了提供接口服务之外,也把上面的前端资源通过静态文件服务器进行提供。

$ npm init -y
$ pnpm install koa @koa/router koa-static koa-bodyparser web-push -S

依赖安装完成之后把基础的Koa代码写一下,把刚才的前端部分的代码放到client目录

import Koa from 'koa'
import serve from 'koa-static'
import router from './router.js'
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import bodyParser from 'koa-bodyparser'
import Router from '@koa/router'

const __dirname = dirname(fileURLToPath(import.meta.url));

const app = new Koa()

const router = new Router({
  prefix: '/api' // 设置统一前缀
});

app.use(bodyParser())
app.use(serve(resolve(__dirname, '../client')))
app.use(router.routes())
  .use(router.allowedMethods())

app.listen(3001, () => {
  console.log('server is running at http://127.0.0.1:3001')
})

现在环境已经完善了,然后来写一个接口,用于下发通知,这里就要利用web-push的能力了。

const PUBLIC_KEY = "公钥"
const PRIVATE_KEY = "私钥"

webpush.setVapidDetails("mailto:1984779164@qq.com", PUBLIC_KEY, PRIVATE_KEY);

router.post('/notice', (ctx, next) => {
  const { title, message, subscription } = ctx.request.body
  webpush.sendNotification(subscription, JSON.stringify({ title, message }));
  ctx.status = 200;
  ctx.body = { success: true, data: true }
})

这里实现的是当接口被请求时,对当前请求接口的设备进行推送,需要根据实际的业务调整,这里仅作示例。

公钥和私钥可以用web-push提供的命令行工具生成:web-push generate-vapid-keys

如果通知下发失败,可以等一会儿看下控制台是否有超时的报错,毕竟用的是google的服务。

或者可以使用FCM SDK来进行推送。

常见问题

正确的代码,但是没有触发通知

这种情况多半是浏览器没有获取到通知权限,浏览器都没有通知权限怎么把能力放给网站呢。Mac系统下需要在【系统设置】->【通知】中打开浏览器通知的权限

image-20240315204807536

Windows系统搜索【通知和操作】,在找到的设置页面打开浏览器通知权限即可。

消息图标未显示

大概率是由于浏览器的跨域策略进行限制,net::ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep 200 (OK),出现类似报错即为跨域限制,将提供图片的服务器开启CORS或者设为同源即可解决。


前端小白