ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

桌面通知API实战:三步实现浏览器消息推送与避坑指南

桌面通知API实战:三步实现浏览器消息推送与避坑指南 做前端这些年被产品和运营追着问最多的一个问题就是怎么让用户愿意回来。页面弹窗用户直接忽略站内信到达率低短信邮件成本高、还得看运营商脸色。直到我把桌面通知API接入项目用户体验一下子不一样了用户只要授权一次哪怕当前浏览器切到后台消息也能像原生App一样弹到屏幕右下角。这篇文章不写虚的直接用3步讲清楚怎么接最后一部分是全网都少有人系统整理过的避坑指南全是真金白银换来的经验。适合后台管理系统、在线客服、协作工具以及一切需要主动触达用户的前端项目。1. 桌面通知API到底能解决什么问题1.1 为什么你需要“主动触达”用户传统的前端提醒方式本质上是“守株待兔”。站内信、红点提示、页头闪烁这些功能只有在用户正开着你的页面时才有效。但用户的真实状态往往是多标签页操作或者干脆把浏览器最小化去做别的事。这时候你再好看的红点提示都躺在那个不活跃的标签页里用户的注意力根本不会分给它。桌面通知API解决的是“通知优先于页面”的问题。它是操作系统级通知显示在屏幕角落跟微信、支付宝、系统邮件的通知长得一模一样。用户已经形成了“看到通知就下意识瞄一眼”的条件反射所以触达效率天然比页面内提醒高一个量级。我见过不少项目一开始用轮询加标题闪烁来做消息提醒效果都很勉强。原因很简单人的视觉注意力很难被标签页里藏着的状态变化拉走但系统通知弹到眼前他想不看都难。1.2 Notification API 的基本能力与适用场景桌面通知API在规范里叫 Notification API是 Web Notifications 标准的一部分。它是浏览器原生能力不需要引入任何第三方SDK也不用额外付费。核心能力可以概括成三点请求权限、创建通知、处理用户交互事件。权限请求机制保证的是“用户可控”避免任意网页都能随意骚扰用户创建通知用来定义通知的标题、正文、图标、分组标识事件回调则让你知道用户是点击了通知、关闭了通知还是根本没理会它。哪类项目最适合接我实际验证下来以下场景收益最明显后台管理系统的待办、审批提醒客服工作台的实时会话弹窗协作工具里的 通知定时任务完成后提醒用户回来查看结果电商后台的订单支付状态变更如果是纯展示型官网或者内容社区我个人不建议为了炫技硬塞这个功能。用户对通知的第一感受很敏感无价值的打扰只会换来一个关闭授权后面再想让他开回来路径长到你怀疑人生。2. 三步搞定通知从权限到事件处理2.1 第一步检测支持并请求权限注意别踩授权的“时机坑”动手第一步不是直接弹通知而是先确认浏览器支不支持。判断方式非常简单// 检查浏览器是否支持桌面通知 if (Notification in window) { console.log(当前浏览器支持桌面通知); } else { console.log(当前浏览器不支持建议提示用户升级浏览器); }这里有一个所有前端新手都会踩的坑权限请求必须发生在用户的手势事件里。也就是点击按钮、点击页面、键盘操作这些“用户主动动作”的回调当中。如果你尝试在页面加载完成后直接调用权限请求很多浏览器会直接拦截Vue的mounted里写、React的useEffect里写基本都不会弹出授权框。我常用的权限请求封装如下兼容了新旧两种API风格function requestNotifyPermission() { if (!(Notification in window)) { return Promise.resolve(unsupported); } // 已经是允许或拒绝状态直接返回避免重复弹窗 if (Notification.permission granted || Notification.permission denied) { return Promise.resolve(Notification.permission); } // 新版 API 返回 Promise旧版 API 是回调式 const result Notification.requestPermission(); if (result typeof result.then function) { return result; } return new Promise((resolve) { Notification.requestPermission((res) { resolve(res); }); }); }调用的正确姿势是在按钮点击事件里调document.getElementById(enableNotifyBtn).addEventListener(click, async () { const permission await requestNotifyPermission(); if (permission granted) { // 已授权可以放心创建通知了 } else { // 被拒绝或忽略这里要给用户一个友好的引导提示 } });另外一个容易被忽略的点是Notification.permission的值只有三种——granted、denied、default。default表示用户还没做选择这时才能触发权限请求弹窗。2.2 第二步用 Notification 构造函数创建通知权限拿到手接下来就是创建通知本体。这一步代码量很小核心是new Notification(title, options)function showNotification(title, options {}) { if (!(Notification in window)) { return null; } if (Notification.permission ! granted) { return null; } const notification new Notification(title, { body: options.body || , icon: options.icon || , tag: options.tag || , dir: options.dir || auto, lang: options.lang || , renotify: options.renotify || false, requireInteraction: options.requireInteraction || false, silent: options.silent || false, data: options.data || {} }); return notification; }这里我把几个常用参数列一下方便你对照自己的需求选参数作用我的建议title通知标题必填控制在20个字以内一眼能看懂body通知正文选填一行能说完不要写成长文icon通知展示的图标建议128x128以上保持跨域可访问tag通知分组标识同一个tag的新通知会替换旧通知是防轰炸利器renotify同tag替换时是否重新提醒需要配合tag使用部分浏览器忽略requireInteraction通知是否保持显示直到用户操作桌面Chrome支持移动端多忽略silent是否静音消息量大的场景建议开避免反复响铃data自定义业务数据点击回调里能取到用于跳转传参创建通知时要记得处理异常。有些浏览器在特定条件下调用new Notification会抛错比如系统级通知服务不可用。稳妥做法是包一层try-catch避免业务主流程被一个警告通知打断。try { const notification showNotification(任务已完成, { body: 导出任务跑完了回来看结果 }); // 继续绑定事件 } catch (e) { console.error(创建通知失败, e); }2.3 第三步绑定事件让通知能点击、能关闭通知弹出来只是开始用户点了通知、关了通知、或者等它超时消失这些动作都需要你用事件接住。最常处理的是四个事件onclick、onshow、onclose、onerror。const notification showNotification(新消息, { body: 张三在工单里回复了你, tag: ticket-123, data: { url: /ticket/detail/123 } }); if (notification) { // 用户点击通知聚焦页面并跳转 notification.onclick function (event) { event.preventDefault(); // 把浏览器窗口拉到前台 window.focus(); // 从 data 里取出业务跳转地址 const targetUrl this.data this.data.url; if (targetUrl) { location.href targetUrl; } // 点击后立即关闭避免残留 this.close(); }; // 通知展示出来后可以设置自动关闭时间 notification.onshow function () { setTimeout(() this.close(), 10000); }; // 关闭时打印日志方便排查 notification.onclose function () { console.log(通知已关闭); }; // 出错时捕获 notification.onerror function (err) { console.error(通知展示失败, err); }; }这里有个细节我反复强调event.preventDefault()一定要写。因为浏览器的默认行为是点击通知后自动聚焦到发起通知的页面如果你同时在 onclick 里做了location.href跳转不阻止默认行为的话会出现焦点刚切过去又被跳转带走的奇怪体验。3. 进阶实战把桌面通知接到真实项目里3.1 场景A任务完成后提醒用户回来查看最典型的场景是异步任务。用户点了一个“导出Excel”按钮导出过程在服务端跑前端在轮询返回结果。这个过程用户没法干等着他可能切去做别的事了。等任务完成光把结果写在页面上没用他不回来看。这时候桌面通知的价值就体现出来了。轮询接口发现任务状态变成 completed立刻弹一条通知async function pollExportTask(taskId) { const timer setInterval(async () { const res await fetch(/api/task/${taskId}).then(r r.json()); if (res.status success) { clearInterval(timer); showNotification(导出任务已完成, { body: 文件已生成点击查看下载, tag: export-${taskId}, data: { url: /export/download/${taskId} } }); } else if (res.status failed) { clearInterval(timer); showNotification(导出任务失败了, { body: res.message, tag: export-${taskId} }); } }, 3000); }这里我用tag做了一层任务维度的去重。如果用户连续触发了多个导出同一个任务只会保留一条通知不会在屏幕右上角堆成一串。3.2 场景B后台工作台的消息即时触达客服工作台和协同工具是另一类典型场景。用户开着工作台页面但切到了别的窗口新的会话进来了、有人回复了工单、审批流走到他这里了这些都需要他“现在就知道”。这种场景我不是用轮询而是用WebSocket推实时消息。收到推送后先判断一个关键条件页面是否处于聚焦状态。如果页面正在前台显示直接更新页面内的红点和列表就可以没必要再弹系统通知否则反而打扰。只有当document.hidden为 true 时才弹桌面通知。socket.onmessage (event) { const message JSON.parse(event.data); if (document.hidden) { showNotification(你有新的工单回复, { body: ${message.from}${message.content}, tag: ticket-${message.ticketId}, data: { url: /ticket/${message.ticketId} } }); } else { // 页面可见静默更新页面内状态即可 updateTicketList(message); } };这个判断非常实用它能有效降低通知的打扰感。用户明明正在你的页面上操作你还往系统里塞一条通知这个体验是减分的。桌面通知的正确用法是“补位”而不是“抢镜”。3.3 避免通知轰炸消息队列与 tag 去重通知消息密集时最常见的翻车现场就是“通知轰炸”。比如用户在线聊天对方连续发了10条消息如果每一条都弹系统通知那屏幕上全是你的通知在刷屏用户脾气再好也要关授权。我实际操作时用了两个策略组合起来效果很好。第一层是 tag 去重按会话维度设置同一个 tag。新通知弹出时浏览器会直接用新通知替换同 tag 的旧通知保证一个会话在系统通知栏里始终只有一条。第二层是节流队列。短时间内频繁触发的通知先丢进队列隔几秒再弹下一条。我封装了一个最小可用的版本let notifyQueue []; let notifyTimer null; const NOTIFY_INTERVAL 4000; // 每次通知间隔4秒 function pushNotify(title, options {}) { notifyQueue.push({ title, options }); if (!notifyTimer) { processQueue(); } } function processQueue() { if (notifyQueue.length 0) { notifyTimer null; return; } const item notifyQueue.shift(); const notification showNotification(item.title, item.options); if (notification) { notification.onshow function () { setTimeout(() this.close(), 8000); }; } notifyTimer setTimeout(processQueue, NOTIFY_INTERVAL); }这两层做下来就算后端一次性推了50条消息用户屏幕上最多也就是每隔几秒出现一条新通知且同一会话始终只占一个坑体验比裸奔好太多。3.4 走得更远Service Worker 与页面级通知的关系这里再补充一个容易混淆的知识点。页面里的new Notification()有一个明显限制页面必须存活才能弹通知。如果用户把浏览器整个关掉了页面级的通知是弹不出来的。想要“浏览器关掉也能收到推送”就需要引入 Service Worker 和 Push API配合服务端的 Web Push 来发送。核心区别在于Service Worker 是独立的浏览器进程即使页面关闭也能处理推送事件并调用self.registration.showNotification()展示通知。// sw.js 中的 push 事件处理 self.addEventListener(push, function (event) { const data event.data.json(); event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: data.icon, data: { url: data.url } }) ); }); // sw.js 中的通知点击处理 self.addEventListener(notificationclick, function (event) { event.notification.close(); event.waitUntil( clients.matchAll({ type: window }).then(function (clientList) { for (let i 0; i clientList.length; i) { const client clientList[i]; if (client.url / focus in client) { return client.focus(); } } if (clients.openWindow) { return clients.openWindow(/); } }) ); });这个方案涉及订阅、后端加密推送载荷、VAPID密钥配置实现成本比页面级通知高不少。但如果你的产品需要“即使关掉页面也触达用户”那是绕不开的路线。我建议先掌握本文的页面级通知再视业务需求升级到 Service Worker 方案。4. 避坑指南我踩过的那些坑4.1 HTTPS、浏览器前缀和开发环境那些事桌面通知API在非安全上下文里会被直接禁掉。也就是说如果你的网站部署在 HTTP 协议下Notification in window这段判断在部分浏览器里会直接返回 false。唯一的例外是localhost本地开发环境不受限制。所以排查问题的顺序一定要对线上环境先看是不是 HTTPS不是的话别的都不用查了。Chrome 从某些版本开始对非安全上下文里的 Notification API 限制非常严格Safari 更是直接不支持。另外早期 Safari 版本需要加window.webkitNotification前缀。如果你需要兼容老设备检查代码要写成const notifySupported Notification in window || webkitNotification in window;不过说实话iOS 上 Safari 对页面级通知的支持一直很弱移动端体验远不如桌面端。所以我一般会在移动端做降级处理检测到移动设备时引导用户使用页面内提醒而不是硬弹系统通知。4.2 权限的坑授权了还弹不出通知多半是这里我把权限相关的坑排在最前面因为这是最容易让前端怀疑人生的地方。第一个坑用户在弹窗里点了“允许”然后发现系统里根本没有通知弹出来。这种情况极大概率是用户在浏览器的“网站设置”里或者操作系统层面把该网站的通知通道关掉了。页面里Notification.permission显示的是 granted但系统层面根本不放行。这个前端无法直接检测只能提供一个“检测通知是否正常展示”的自测入口点击后弹一条测试通知弹不出来的话引导用户去浏览器设置里检查。第二个坑权限请求弹窗只能主动拉起一次。用户点了“忽略”或者“关闭弹窗”permission会保持default此时你再次调用requestPermission()在很多浏览器里不会有任何反应。用户明确点了“拒绝”之后更麻烦permission变成denied后续再怎么调requestPermission()都不会再弹窗唯一的恢复路径是让用户去浏览器设置里手动改权限。所以前端要做的是在页面里留一个“通知已关闭如何重新开启”的引导说明常见做法是弹一个浮层写清楚去浏览器设置里怎么找回这个站点的通知权限。别在这时候写一段毫无帮助的“请检查浏览器设置”用户看了等于没看要具体到“点击地址栏左侧图标 - 网站设置 - 通知 - 改为询问/允许”。4.3 交互细节的坑点击、关闭、聚焦交互层面也有不少细节不注意的话会出现“通知弹出后成了僵尸”的体验。第一个细节是点击通知必须close()。如果不手动关闭很多浏览器在用户点击通知后不会自动收起它那条通知就会一直挂在系统通知栏里用户再点一次发现又触发了一次跳转体验很糟糕。第二个细节是通知点击后的焦点问题。浏览器默认行为会把焦点还给发起通知的页面但如果你在回调里做了一些异步操作最好显式调用window.focus()或者window.open(url, _blank)确保页面窗口确实到了前台。第三个细节是requireInteraction的使用要克制。这个参数设为 true 后通知会一直挂着不自动消失。对高优消息来说很有用但如果所有消息都设成 true系统通知栏会被你的攒出一排通知反而让用户反感。我的习惯是只对“审批到期”“订单异常”这类必须处理的强提醒开启。第四个细节是通知内容的安全问题。title 和 body 都是纯文本但如果你把用户输入的内容直接拼进去且没做转义处理理论上会面临格式混乱或注入风险。处理用户昵称、用户签名这类数据时建议统一做一遍 HTML 实体转义别偷懒。4.4 常见问题速查表我把实际遇到的典型问题整理成了一张速查表方便你排查时直接对照现象排查方向解决办法点击授权按钮没反应请求不在用户手势回调里把requestPermission放进 click 事件里调用授权后仍然弹不出通知系统或浏览器设置关闭了通知引导用户检查网站设置并保留自测通道通知刚弹出就消失未设置requireInteraction按业务需求开启该参数点击通知没跳转没绑定 onclick 或没关闭默认行为绑定 onclick 并调用event.preventDefault()iOS Safari 不弹通知平台能力限制降级为页面内提醒或改为 Web Push 方案通知内容出现乱码用户输入未做转义展示前统一做 HTML 转义处理通知在系统里堆积成串没使用 tag 去重按会话/任务维度设置 tag同一 tag 通知替换后不响renotify未配置需要时开启 renotify 并配合 tag 使用在做桌面通知API之前我一度先入为主地觉得这只是个两三行代码的小API接上就能用。真在业务里打磨过一遍才发现参数背后是权限机制、用户习惯、系统交互的多层博弈。所有细节做扎实这个功能才会成为产品的留存抓手而不是用户嘴里“那个网站天天弹广告”的吐槽素材。我个人的习惯是所有通知必带场景化的 tag点击后统一关闭同时保留一个全局静音开关。技术实现反而不是最难的难的是帮用户守住那一份“不想被打扰”的体面。这也是我踩过一堆坑之后最想告诉你的经验。
返回列表