首页/文章/javascript

你能让网页也像QQ那种一样发出右下角消息吗

2024-09-05
354912 分钟
...

需求

很平常的一个工作日,「产品经理」给我发了个消息:

产品:你能让网页也像QQ一样,来了消息能进行提示吗?我说的不是网页内的弹出消息

我:刚搞完上个需求,别搞嗷,你小子


产品是一个比较随和的人,所以上面的对话比较随意;具体的聊天内容,我这里就省略了,跟他对话之后,他的大致需求如下:

  • 页面中展示预警内容

  • 检测到预警情况会通知用户且更新页面内容

  • 用户不会一直将页面展示在最前面,所以需要做到页面即使被遮挡、收起,用户也能收到提示,达到及时提示的目的

解析

把功能点解析一下,得出了以下内容:

  • 需要使用WebSocket来监听后端消息,收到消息之后进行通知用户和更新页面内容

  • 寻找一种方式来发出通知

其中的难度在于第二点,找哪种方式来发出通知?我的第一反应其实有两个选项:

  • electron处理

  • BOM API上想办法

electron的主进程是有办法来处理发出通知的操作的,只是那时候我在团队内部引入electron的技术点还属于测试阶段,完整Demo没有走通,卡在了最后的发布更新这一阶段。所以如果当时采用这个方案,后续的处理,不稳定也不可控。

基于以上考虑,我最终把目光放到了BOM API上面,我想起了看过的红宝书中,有一个关于浏览器通知的知识点,可能可以处理这个需求。

调研(展示效果基于windows的谷歌浏览器)

在查看了红宝书内容之后,得到了一个BOM APINotification,这个API可以让页面发出通知,基本用法如下:

`function notifyMe() {
  if (!("Notification" in window)) {
    // 检查浏览器是否支持通知
    alert("当前浏览器不支持桌面通知");
  } else if (Notification.permission === "granted") {
    // 检查是否已授予通知权限;如果是的话,创建一个通知
    const notification = new Notification("你好!");
    // …
  } else if (Notification.permission !== "denied") {
    // 我们需要征求用户的许可
    Notification.requestPermission().then((permission) => {
      // 如果用户接受,我们就创建一个通知
      if (permission === "granted") {
        const notification = new Notification("你好!");
        // …
      }
    });
  }

// 最后,如果用户拒绝了通知,并且你想尊重用户的选择,则无需再打扰他们
}

`

这个例子中涉及三个东西:

  • Notification.permission,当前通知的权限,有granteddenieddefault三个值,分别表示允许、拒绝、默认

  • Notification.requestPermission,请求当前页面的通知权限,调用这个方法,浏览器左上角会出现询问窗口,表示是否允许该页面发出通知

  • new Notification("你好!"),发出通知的简易代码。

如果你出于好奇,在本地服务执行了上面的代码,那么你的右下角就会有这样一个东西,并且隔一会儿就消失:

image.png

Notification

接下来讲一下这个API如何使用:

  1. 构造函数

new Notification(title) new Notification(title, options)

  • title(必填),通知的标题,显示在通知窗口的顶部

  • options(可选),可选配置项

    属性说明
    body一个表示通知正文的字符串,显示在标题下方,默认值是一个空字符串
    icon一个包含要在通知中显示的图标的 URL 的字符串
    image一个包含要在通知中显示的图像的 URL 的字符串
    requireInteraction指示通知应保持活动状态,直到用户单击或关闭它,而不是自动关闭。默认值为 false
    ......

在这里的配置选项中,我只列举了四个常用属性,其余属性,可以自行到_MDN_(https://developer.mozilla.org/zh-CN/docs/Web/API/Notification)查看;使用以上属性可以构建一个比较丰富的通知:

new Notification('这是标题', {   body: '这是正文',   icon: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',   requireInteraction: true,   image:     'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png', })

image.png

  1. 实例方法 每一个创建的通知实例都有一个close方法,用于关闭或移除先前显示的通知。

`const notif = new Notification('这是标题');

setTimeout(() => {
    notif.close();
}, 500);

`

  1. 事件

可注册的事件有四类

  • click:当用户点击通知时触发

  • close:当用户关闭通知时触发

  • error:当通知发生错误时触发

  • show:当通知显示时触发

`const notif = new Notification('这是标题');

notif.onclick = () => {
    console.log('点击了通知');
};

notif.onclose = () => {
    console.log('关闭了通知');
};

notif.onerror = () => {
    console.log('通知出错');
};

notif.onshow = () => {
    console.log('显示了通知');
};

`

基础Demo

ok,走到这里,通知或者说消息的问题已经有眉目了;那么就到了处理业务逻辑的时候了。

先来理一下Demo中需要实现的逻辑(Demo页面使用ReactWebSocket使用expressws实现):

  • 构建一个WebSocket,接收后端消息,模拟预警情况:这里我用expressws来模拟

  • 处理接收到预警情况之后的发送通知逻辑更新页面内容逻辑:发送通知使用Notification API,更新页面内容用收到后端消息,新增一条表格数据来模拟

`// WebSocket,用node运行此文件
const express = require('express');
const WebSocket = require('ws');
const WebSocketServer = WebSocket.WebSocketServer;

const app = express();

app.get('/', (req, res) => {
  res.send({
    ok: 1,
  });
});

app.listen(3000, () => {
  console.log('express start');
});

const wss = new WebSocketServer({port: 8080});

wss.on('connection', function connection(ws) {
  ws.on('error', console.error);

ws.on('message', function message(data) {
    console.log('received: %s', data);
  });
  // 每4s发送一条消息
  setInterval(() => {
    ws.send('something', {binary: false});
  }, 4000);
});

`

`// 两个关键方法

// 请求用户通知权限,需要用户点击允许
const notificationSet = () => {
if (!('Notification' in window)) {
// 检查浏览器是否支持通知,不可用则显示页面提示
setVisible(true);
} else if (Notification.permission === 'granted') {
// 检查是否已授予通知权限;如果是的话,创建一个默认通知,表明后续采用此种方式提示用户
initNotifyPermission();
} else if (Notification.permission === 'default') {
// 默认则征求用户的许可
Notification.requestPermission().then((permission) => {
// 如果用户接受,我们就创建一个通知
if (permission === 'granted') {
initNotifyPermission();
} else {
// 用户选择拒绝,则进行message提示
messageApi.open({
type: 'error',
content:
'您禁用了该页面发出通知,这将导致您收不到后续更新提示,请在浏览器设置中放开该页面通知权限',
duration: 10,
});
}
});
}
};

// 建立websocket连接
const webSocketSet = () => {
const socket = new WebSocket('ws://localhost:8080');

// 监听连接  
socket.addEventListener('open', function () {  
    socket.send('Hello Server!');  
});

// 监听后端传来的消息  
socket.addEventListener('message', function (event) {  
    // 用新增一行table来标识接受到了消息  
    setTableData((data) => [  
        ...data,  
        {  
            key: Math.random().toString(36).slice(-6),  
            name: 'Joe Black',  
            age: 32,  
            address: 'Sydney No. 1 Lake Park',  
            tags: ['cool', 'teacher'],  
        },  
    ]);  
    // 发出通知  
    new Notification('数据已更新,请立即查看', {  
        body: '具体信息。。。',  
        icon: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',  
        requireInteraction: true,  
        image:  
        'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',  
    });  
});

};

// 进入页面请求页面通知权限、建立WebSocket链接

useEffect(() => {
notificationSet();
webSocketSet();
}, []);

`

限制

如果你看到这里还没有选择退出,表明你可能真的要用这个知识点,也可能你跟我一样也是在做技术调研,坏消息就是这个API有几个限制点,你需要考量一下:

  • HTTPS:此项功能仅在一些支持的浏览器的安全上下文(HTTPS)中可用;也就是说页面访问的地址需要是https的;在本地调试时,要注意以下情况:

    image.png

  • Local: http://localhost:8080/ 可以使用这个API

  • Network: http://xxx.xx.xx.xx:8080/不能使用

  • 浏览器通知权限:通知能否显示,受浏览器对该页面的权限控制,用户点击拒绝之后,只有提示用户去浏览器设置中,将该页面通知权限打开才能正常使用

    image.png

  • 系统通知权限:通知能否显示,也受系统对浏览器的权限控制;若是浏览器被系统禁止发出通知,即使网页允许发送,最终也看不到通知,也需要用户去系统设置中打开对浏览器的通知权限。

    image.png

  • 免打扰模式:某些系统有免打扰模式,如果这个模式被开启,那么也是接受不到通知的

  • 浏览器差异:各个浏览器(Chrome、Safari、Edge等)对通知的外观、逻辑等实现有差异,这些差异是否会影响你的具体需求,需要自行测试之后斟酌

  • 兼容性:此处是_浏览器兼容情况_(https://developer.mozilla.org/zh-CN/docs/Web/API/Notification#浏览器兼容性),请查阅

瑕疵

上面的基础Demo已经能实现接收后端消息后更新页面并发出通知,但是如果你运行了上面的代码,会发现还存在一些问题:

  1. 由于设置的是通知不自动关闭,发出多个通知之后,需要手动关闭,很麻烦

    解决方式:发出的通知收集到一个数组里面,统一管理。

  2. 点击通知,不能回到页面中

    解决方式:给通知的点击事件添加如下代码:

    notif.onclick = () => {   window.focus(); };

  3. 页面可见的时候不需要发出通知,不可见才发出通知

    解决方式:利用_visibilityState属性_发出通知之前,判断页面可见性:

    if (document.visibilityState !== 'visible') {     // 发出通知 }

  4. 页面可见时,关闭之前的通知

    解决方式:利用_visibilitychange事件_(https://developer.mozilla.org/zh-CN/docs/Web/API/Document/visibilitychange\_event)监听页面可见性变化

    document.addEventListener('visibilitychange', () => {     // 关闭之前的通知 });

  5. 没有通知音效,提醒效果不明显

    解决方式:页面增加一个audio元素,设置为隐藏,手动控制播放

    `<audio
       src={warmTipsAudio}
       style={{ display: 'none' }}
       ref={warmTipsAudioRef}

    `

    warmTipsAudioRef.current.play();

完整Demo代码

我把完整的Demo代码,放到_这里_(https://gist.github.com/StudyDayByDay/9371d0b78006055b512ca8d01f5df045)了(需要科学上网),一共两个文件:

  • WebSocket的文件(express+ws),可以在package.json配置一个类似"ws": "node ./web/index.ts"的命令来启动

  • React+antd的页面

PS:音频文件没法提供,可以使用_ttsmaker 文本转语音工具_(https://ttsmaker.com/zh-cn)自己生成一个放到项目中去。

扩展

这里还有一个额外的知识,就是我发现某些网页能够实现:即使浏览器没有打开,也可以发送通知的功能,这个我去了解了一下,大致原理如下,有需要的朋友自行阅读:

「即使浏览器未打开,某些网页也能发出桌面通知」,通常是通过浏览器推送通知(Browser Push Notifications)技术实现的。具体来说,这项功能依赖于以下几个关键技术组件:

  1. 服务工作线程(Service Workers)
    服务工作线程是一种独立于网页主线程的脚本,能够在后台运行,即使浏览器未打开,或者用户没有打开特定网站。它们在浏览器中作为一个中间层,处理网络请求、推送通知、缓存等任务。服务工作线程可以在后台保持活跃,并在接收到来自服务器的推送消息时触发通知。

  2. 推送通知(Push Notifications)
    推送通知是由服务工作线程处理的消息。这些通知通过推送服务从服务器发送到用户的浏览器,即使用户当前没有访问该网站。推送服务将消息传递到浏览器中的服务工作线程,后者可以根据消息的内容在桌面上显示通知。

  3. Web 推送 API(Web Push API)
    这是一个标准化的 API,允许网站在用户授权后注册一个服务工作线程,并通过推送服务发送通知。网站首先需要获得用户的许可,才能使用这项功能。一旦用户同意,网站可以在服务工作线程中注册推送事件,并配置通知的内容。

  4. 浏览器推送服务(Browser Push Service)
    各大浏览器都有自己的推送服务,用于中转来自网站的推送消息。这些服务确保消息能够在合适的时间传递给浏览器,即使用户未打开浏览器。这些推送服务通常与操作系统通知系统集成,确保通知能在桌面上显示。

工作流程概述

  • 用户授权:当用户访问支持推送通知的网站时,网站会请求通知权限。

  • 服务工作线程注册:如果用户同意,浏览器会在后台注册一个服务工作线程。

  • 推送通知:网站服务器通过浏览器的推送服务向用户的设备发送通知,即使浏览器未打开,服务工作线程也能接收到这些消息,并在桌面上显示通知。

这种机制使得网站能够在用户没有主动访问时,仍然与用户保持互动,提供及时的信息或提醒。

总结

本文从需求调研的角度出发,研究了浏览器发出通知的方式,讲解了Notification API的使用和其局限性,实现了一个小Demo,最后探索了浏览器不打开也能发出通知的实现方式。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2026-02-24
navigator.sendBeacon全指南
在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户 关闭页面 、 刷新 或 跳转路由 时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。 但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— 。 一、 痛点与传统方案的挣扎 场景还原 当用户点击关闭按钮时,浏览器会触发生命周期事件( 或 )。如果我们直接使用普通的异步 AJAX ( 或 ) 发送请求,浏览...
学习
javascript2025-11-02
理解浏览器事件系统,从用户点击到事件对象的完整旅程
深入理解浏览器事件系统:从用户点击到事件对象的完整旅程 “当我点击页面按钮时,背后发生了什么?为什么回调函数能收到一个包含丰富信息的event对象?今天,让我们一起揭开浏览器事件系统的神秘面纱。” 一个令人困惑的现象 作为前端开发者,我们每天都在写这样的代码: 这段代码如此熟悉,以至于我们很少停下来思考:​ ​这个 对象到底从哪里来?它为什么能知道点击的精确坐标?为什么能识别是哪个元素被点击了?​ ​ 更神奇的是,当我们手动创建事件时:...
学习
javascript2025-10-01
实现大文件上传全流程详解
在日常开发中,大文件上传是个绕不开的坎——动辄几百 MB 甚至 GB 级的文件,直接上传不仅容易超时,还会让用户体验大打折扣。最近我用 Vue+Express 实现了一套完整的大文件上传方案,支持分片上传、断点续传、秒传和手动中。 一、先看效果:我们要实现什么? 先上核心功能清单,确保大家明确目标,知道我们要解决哪些实际问题: 大文件分片上传 :将文件切成固定大小的小片段分批上传,避免单次请求超时 秒传 :服务器已存在完整文件时,直接返...
学习
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-15
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一遍 1\. 最偷懒:原生 一行代码就能跑,浏览器帮你搞定。 🔍 关键决策点 2020 年后现代浏览器全覆盖,IE 全军覆没。 必须写死 ,否则 CLS 会抖成 PPT。 适用场景...
学习
javascript2025-09-10
🚀 Web Worker让你的应用丝滑
🌟 引言 在日常的前端开发中,你是否遇到过这样的困扰: 大数据处理时页面卡死 :处理几万条数据时,页面直接卡成PPT,用户点击毫无反应 复杂计算阻塞UI :图片处理、数据分析等计算密集型任务让整个应用假死 文件上传/下载卡顿 :大文件操作时,其他功能完全无法使用 实时数据处理性能差 :WebSocket接收大量数据时,页面渲染严重滞后 今天分享6个Web Worker的核心技巧,让你的应用告别卡顿,用户体验丝滑如德芙! 💡 核心技巧...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录