资讯动态

浏览器系统通知实战:Notification API与Service Worker

发布时间:2026/10/22 8:30:59 来源:尧图企业网站定制
1. 一个“关掉标签页也要看见提醒”的真实需求为什么最后选了Notification1.1 业务场景与三条可选技术路线上个月我接手一个内部工单系统的前端改造业务方提了一个很实在的需求新工单分配下来时就算用户没把浏览器停在工单页面上只要系统后台能拿到消息就要第一时间把信息递到用户眼前。当时负责这个系统的大哥原话是“不要光在页面里放个红点我们要的是那种电脑右下角弹出来的系统通知。”这个需求听起来简单做起来牵扯的东西不少。我先梳理了候选技术路线一共三条页面内站内信加红点提醒前端可以定时轮询接口拿到新数据后在页面内弹出提示框。缺点是用户标签页一旦切到别处或者窗口被最小化提示就相当于石沉大海。WebSocket实时推送加页面内提示能保证数据到达的实时性但“呈现”这一环仍然受制于浏览器页面状态。用户没看这个标签页提醒一样会埋没在一堆标签里。浏览器原生通知也就是浏览器标题里的Notification API它可以把消息以系统通知的形式弹到操作系统桌面右下角不依赖当前页面是否在前台也不需要引入第三方推送服务。最终我选了第三条。核心原因不是它多“高级”而是它恰恰踩中了这个场景的命门消息到达的层级要从浏览器页面提升到操作系统层面。一是在标签页被切走的情况下系统通知依然能覆盖到用户视线二是Notification API是浏览器原生能力不需要为了一张“桌面通知”的皮去接入一套完整推送SDK三是它和Service Worker结合后甚至可以在页面完全关闭的情况下由后台脚本收到推送并调起系统通知。1.2 Notification API的能力边界做技术选型最忌讳把工具的边界搞混。Notification API不是万能的它在实际业务里能做和不能做的事情要在一开始就说清楚。它能做的弹一条带标题、正文、图标的系统通知显示在操作系统通知中心。通过tag参数管理同类型通知避免重复刷屏。监听用户对通知的点击、关闭等行为实现“点击通知跳转到业务详情页”。配合Service Worker在页面关闭后依然展示通知。通过actions参数在通知上直接提供操作按钮部分桌面浏览器支持。它不能做的不能绕过用户授权。几乎所有现代浏览器都要求用户主动点击“允许”并且页面必须运行在安全上下文HTTPS或localhost下。不能在没有Service Worker的情况下做到“页面完全关闭后还能弹通知”。不能保证通知一定伴随声音。系统免打扰模式和浏览器自身的通知设置都会影响声音行为这是前端代码控制不了的。不能跨设备。它只在当前浏览器所在的操作系统上展示移动端的支持程度也远不如桌面端。提示如果你把Notification API理解成“浏览器帮我弹出一条桌面弹窗”那这个认知基本准确但如果你以为它能替代整套消息推送平台那后面项目大概率会翻车。明白了边界之后整个实现的路径就清晰了先解决权限申请再学会构造通知然后用Service Worker补上“后台推送”的缺口最后处理各种浏览器、操作系统层面的奇怪问题。2. 权限申请这步的细枝末节状态机、授权条件与降级策略2.1 permission状态机与第一次弹窗Notification API真正开始跑之前必须先过权限这道门。Notification.permission一共三种状态状态含义用户能做什么default未询问过等同于拒绝浏览器根据站点设置决定是否出现授权弹窗granted已允许可以正常弹通知denied已拒绝无法再弹出通知也不能通过代码再次弹授权框后面这个“denied后无法再弹授权框”非常关键。很多新手第一次做时在页面上直接调Notification.requestPermission()用户手滑点了“阻止”然后该项目在浏览器里的权限就永久锁死了。用户只能手动去浏览器设置里改回来代码是救不了的。所以我做的第一件事不是急着调API而是先写一个权限状态的读取逻辑function checkNotificationPermission() { if (!(Notification in window)) { console.warn(当前浏览器不支持Notification API); return unsupported; } return Notification.permission; }注意这里Notification in window的判断。低版本浏览器、部分移动端WebView里压根没有这个对象直接调用会抛异常。先做能力检测是最稳妥的起步。2.2 HTTP限制、用户手势与授权弹窗Notification.requestPermission()有两个非常容易被忽略的前提。第一个是安全上下文。线上环境必须是HTTPS才能正常调用http://的站点在部分浏览器里直接拿不到权限控制台会提示“API can only be used in secure contexts”。本地开发时localhost和127.0.0.1会被浏览器当成安全上下文所以本地调试不会有问题等到了测试环境换成IP地址访问问题就暴露出来了。这个坑我在某次联调时踩过同事拿http://192.168.x.x:8080访问系统Notification直接不可用排查了半天才反应过来。第二个是用户手势。Chrome对requestPermission()有严格的调用时机限制它必须处于用户触发的事件回调里比如click、keydown这类交互事件。如果你在页面加载完成后直接调用或者在setTimeout回调里调用浏览器会直接忽略这次请求甚至不弹出授权框控制台会给你一句“Permission request must be called in a user gesture handler”。正确写法一般是const enableNotificationBtn document.getElementById(enable-notification); enableNotificationBtn.addEventListener(click, async () { if (!(Notification in window)) return; const permission await Notification.requestPermission(); if (permission granted) { showSuccessTip(通知权限已开启); } else if (permission denied) { showPermissionGuide(); } });把授权动作绑在一个明确的按钮上既符合浏览器要求也给了用户一个缓冲和决策的入口。后面在用户体验部分我会再展开说明。2.3 权限被拒后的降级策略权限被拒这件事产品经理往往不会提前告诉你但你必须提前想好。我在这套工单系统里做了三层降级第一层如果Notification.permission denied在业务页面顶部展示一条非入侵式提示条写明“浏览器通知权限已被拒绝可能导致新工单提醒无法送达”并附带一个“如何重新开启”的帮助入口引导用户去浏览器设置里手动改回。第二层代码层面把“站内红点页面内Toast”作为兜底。权限被拒了系统通知这条链路断了但站内提醒不能一起断。用户至少还能在打开系统时看到未读数量。第三层将权限状态同步到后端在用户列表页标记“低提醒可用性用户”方便业务方后续安排人工触达或者邮件通知。这条是我后来加上的因为有一次测试发现好几个同事的权限是denied但他们自己完全不知道。注意降级不是可选项而是必须项。Notification API的权限完全掌握在用户和操作系统手里你的代码只能申请不能强迫。没有降级方案就相当于把一个核心提醒功能寄托在用户的一念之间。3. 构造一条真正有用的系统通知参数、去重与点击行为3.1 构造参数逐个梳理权限拿到手之后创建通知的API非常简单new Notification(title, options)一行就能弹出来。但options里的参数很值得逐一说清楚因为参数用得对不对直接决定这条通知是“能看”还是“好用”。const notification new Notification(新工单待处理, { body: 工单 #10245 已分配给你请及时处理。, icon: /images/logo.png, badge: /images/badge.png, tag: work-order-10245, requireInteraction: true, vibrate: [200, 100, 200], data: { orderId: 10245, url: /work-order/detail/10245 } });我重点讲几个平时不太被注意的参数body通知的详情正文建议控制在两行以内。系统通知弹窗本身就是给用户扫一眼的写太多字没人会认真看反而显得冗长。实际测试中文场景下正文超过60字就会在通知中心里被截断。badge小图标在通知栏里显示的缩略图标移动端支持得更多桌面端部分浏览器会忽略。建议准备一个白色的纯色图标尺寸不用太大。tag通知的标识。如果前后两条通知使用同一个tag后一条会替换前一条而不是再叠加一条新通知。这个参数在消息流场景里非常有用。requireInteraction默认是false意味着通知会在系统设定的时间后自动消失。如果希望通知一直停留到用户主动处理需要设成true。这个参数很关键工单提醒类场景建议打开但也要考虑通知堆积的问题。data自定义数据可以塞业务ID、跳转URL等通知被点击后这些数据会原样带出。几乎每次做通知跳转都离不开它。vibrate振动模式数组控制振动节奏。只有部分移动端浏览器支持桌面端基本忽略。3.2 避免通知刷屏tag与通知合并消息通知有一个很常见的体验灾难后端一次性推了10条新工单前端傻乎乎地弹了10条通知用户的屏幕右下角瞬间被塞满。桌面系统会堆一个长长的通知列表用户体验直接归零。处理方式有两种第一种是后端聚合把一段时间内的多条消息合并成一条比如“您有5条新工单待处理”前端只弹一次。这种方案对后端接口有要求但效果最好。第二种是前端用tag控制。当同一个tag的通知已经存在时新通知会替换旧通知。比如你按工单号做tag同一个工单的提醒反复弹的时候就只会显示最后一条不会越积越多。如果不想用业务ID做tag也可以用一个固定tag如new-work-order那同一段时间内所有新工单通知都会合并成一条。实际项目里我更喜欢“按业务ID分类、按时间聚合”的组合方案单条重要通知用独立tag批量通知用聚合tag。3.3 点击通知跳转到业务页通知弹出去只是第一步用户点击通知后要能回到对应的业务页面这才是闭环。Notification实例上有onclick事件配合构造函数里传入的data字段跳转逻辑非常简单notification.onclick function (event) { event.preventDefault(); const url this.data.url || /; window.focus(); if (url) { window.open(url, _blank); } this.close(); };这里有两个细节需要说明一个是event.preventDefault()。在部分浏览器里如果你不阻止默认行为浏览器的默认表现是“聚焦到产生通知的那个页面”而不是打开你想要的URL。所以要跳转需要手动阻止默认行为后再处理。另一个是window.focus()。如果浏览器窗口被最小化了先聚焦窗口再打开新页面用户看到的层级会顺畅很多。我实测在Windows的Chrome和Edge上只执行window.open有时会“弹不出来”加上window.focus()就稳定很多。3.4 用actions参数做通知按钮桌面端Chrome和Edge支持在通知上直接挂操作按钮最多两个。这个能力很适合做“稍后处理”和“立即查看”这类轻量操作const notification new Notification(新工单待处理, { body: 工单 #10245 已分配给你, tag: work-order-10245, actions: [ { action: open, title: 查看详情 }, { action: later, title: 稍后处理 } ], data: { url: /work-order/detail/10245 } }); notification.onclick function () { // 点击通知主体 window.focus(); window.open(this.data.url, _blank); }; notification.onaction function (event) { if (event.action later) { this.close(); } };注意是onaction而不是onclick处理按钮事件这个事件属性让很多第一次接触的人栽了跟头。另外macOS上通知按钮的表现和Windows不太一样需要各自适配不能假设一套代码全平台通吃。4. Service Worker 配合使用页面关闭后通知仍然能弹的关键4.1 为什么必须引入Service Worker只用new Notification()有一个硬伤它是页面脚本的一部分页面没开或者标签页被关了脚本不执行通知自然弹不出来。而业务里“用户没打开系统也要收到工单提醒”的诉求恰好需要一条脱离页面存在的后台通道。这就是Service Worker的用武之地。Service Worker是一个独立于页面的后台脚本浏览器会把它的生命周期交给站点注册关系来管理不需要页面保持打开。它跟Notification API的组合才是“浏览器系统通知”真正完整的状态。在我这个工单系统里技术方案是这么定的前端注册Service Worker页面在用户授权后、登录完成后把当前用户的信息通过postMessage传给Service WorkerService Worker里维护一个定时轮询任务发现新工单后直接调self.registration.showNotification()把系统通知弹出来。4.2 showNotification与new Notification的关键区别在Service Worker里弹通知不能用new Notification()这个API需要在页面上下文里用。Service Worker里对应的API是self.registration.showNotification(title, options)。两者在大部分参数上是统一的但有几个区别值得说showNotification方法由registration对象提供不需要额外权限检测因为注册Service Worker本身就已经隐含了站点信任关系。通过Service Worker弹出来的通知点击事件由notificationclick事件监听而不是onclick属性。Service Worker里可以调clients.matchAll()之类的接口判断当前是否有打开的页面再决定要不要通知。这给了前端很大的控制权。一个完整的Service Worker核心片段大概是这样self.addEventListener(message, (event) { if (event.data event.data.type CHECK_WORK_ORDERS) { checkWorkOrdersAndNotify(); } }); async function checkWorkOrdersAndNotify() { try { const response await fetch(/api/work-orders/latest); const orders await response.json(); if (orders.length 0) { const title 你有 ${orders.length} 条新工单; const body orders.map((o) #${o.id} ${o.title}).join(\n); await self.registration.showNotification(title, { body, icon: /images/logo.png, badge: /images/badge.png, tag: work-order-latest, data: { url: /work-order/list?new1 } }); } } catch (error) { console.error([SW] 检查工单失败, error); } } self.addEventListener(notificationclick, (event) { event.notification.close(); event.waitUntil( clients.matchAll({ type: window, includeUncontrolled: true }).then((windowClients) { for (const client of windowClients) { if (focus in client) { client.focus(); client.postMessage({ type: OPEN_URL, url: event.notification.data.url }); return; } } return clients.openWindow(event.notification.data.url); }) ); });4.3 定时轮询提醒的完整演练在Service Worker里做定时轮询需要用到setInterval吗并不合适。Service Worker的生命周期以事件驱动定时器在空闲时可能被挂起运行时间不可靠。更稳的做法是配合self.registration的周期或事件触发机制。但实际开发中一个简单可行的方法是在页面打开时用setInterval主动向Service Worker发起检查请求同时依赖服务器的推送或下次页面打开时的兜底。由于我们没有接入第三方推送服务这里采取的是“页面活跃时轮询Service Worker定时兜底”的组合。页面端的定时器如下// 页面登录成功之后 if (serviceWorker in navigator Notification.permission granted) { const registration await navigator.serviceWorker.register(/sw.js); setInterval(() { if (navigator.onLine) { registration.active.postMessage({ type: CHECK_WORK_ORDERS, silent: true }); } }, 5 * 60 * 1000); }Service Worker端再接一个install或activate后的兜底形式可以是setTimeout一次性延迟检查避免页面刚恢复时服务端拿不到最新流程。注意不要试图在Service Worker里维护一个长期“心跳定时器”它会因为生命周期策略被中途杀掉。轮询的真正可靠源头还是页面的setInterval挂载。4.4 Service Worker调试要点与常见失手Service Worker的调试和普通页面脚本不一样很多新人第一次上手会找不到入口。这里给几条实测经验第一更新Service Worker脚本后需要重新刷新才能生效。Chrome DevTools的Application面板里Service Workers区域有个“Update”按钮和更新时间显示更新完必须手动触发。第二Service Worker不会立即控制当前页面。用navigator.serviceWorker.register(/sw.js)注册后当前页面默认不受控需要registration.active.postMessage时先确认active状态不为null。我在这个项目里就踩过页面刷新得太快Service Worker还没激活registration.active是nullpostMessage直接抛异常。后来加了一行保护if (registration.active registration.active.state activated) { registration.active.postMessage({ type: CHECK_WORK_ORDERS }); }第三Service Worker里的console.log输出要在DevTools的“Service Workers”面板里看不能用普通页面的Console过滤条件。我自己调试时经常习惯性打开页面Console结果找半天看不到自己打印的日志。5. 实测踩坑声音、图标、免打扰状态和权限“卡死”5.1 通知不出声往往不是代码问题工单系统上线后的第三天有同事反馈“弹通知了但没有声音”测试群里炸了锅。我在代码里根本没写控制声音的逻辑一开始还挺冤枉后来越查越明白这个过程值得完整复述一遍。第一个俄刻出的环节是浏览器设置。Chrome的站点设置里“通知”权限旁边还有一项“声音”需要单独允许。某些企业浏览器策略会把通知声音默认关闭。我当时在公司测试机上打开chrome://settings/content/notifications一级一级看下来才发现站点被分组为“静音通知”类别。第二个环节是操作系统层面。Windows 10/11的“专注助手”开启时所有通知都会静默macOS的“勿扰模式”也会吞掉声音。这两种状态从浏览器DevTools里完全看不出来只能肉眼去看系统状态栏。项目组的测试机各种版本混杂最后我加了一条调试思路先明确通知本身确实弹出来了再逐层排查声音被哪一层吞掉。这个“自顶向下”的排查顺序看起来很简单但实际面对问题时很容易在瞎猜里浪费一晚上。5.2 图标不显示的排查链路通知图标不显示也是高频问题。现象是文字内容正常弹出来但图标位置是空白的。这个问题的排查链路偏简单却很有代表性。第一步查图标路径。icon字段的URL必须是页面可访问的绝对或相对路径权限认证后的静态资源在通知里不一定生效。我的项目因为前端部署在CDNlogo图标带时间戳签名通知里直接裂了。换成本地静态路径或允许公开访问的CDN地址后问题消失。第二步查图标格式。部分浏览器要求图标必须是PNG、JPG等常见格式WebP有时不被识别。工单系统原来的logo是WebP换成PNG之后才稳定。第三步看尺寸。Chrome桌面端在Windows上对通知图标的展示区域并不大建议使用128x128的图标不要太依赖超高清素材。测试时我发现部分图标文件过大超过1MB会导致渲染延迟通知弹出来时图标位置一闪而过的空白半秒后才补上。5.3 权限状态“卡死”在default怎么办实际项目里还有过一个更隐蔽的情况用户并没有明确拒绝但Notification.permission一直返回default而且requestPermission()不再弹出浏览器授权框。这是怎么发生的Chrome的权限机制里同一个来源origin如果被用户主动“阻止”过或者授权框被用户忽略多次浏览器会自动视为拒绝并把站点加入“停止询问”列表。这时候代码层面的requestPermission()不会再触发授权框权限状态停留在default表现就是“卡死”。这种情况没有编程解法只能引导用户手动修改站点权限。我在项目里的做法是检测到default且requestPermission()的返回值在合理时间后仍非granted时弹出“请手动开启通知权限”的操作指引并附上一张截图式的步骤说明浏览器设置入口从哪进、找到我们的站点、把通知改成允许。这个指引文案要具体到浏览器版本因为Chrome和Edge的设置路径完全不同。5.4 通知堆积的体验教训上线一段时间后另一个问题冒出来了有些用户一天下来通知中心堆积了几十条通知列表滚都滚不完。我反思了一下根子在requireInteraction: true上。requireInteraction: true让通知不会自动消失工单系统的本意是“重要消息不能被忽略”但实际效果是通知中心成了信息垃圾场。后来我调整了策略只对紧急工单如 P1 级别用独立通知且requireInteraction: true其余批量工单通知统一打tag自动折叠合并并且设回requireInteraction: false让它在几秒后自然淡出。用户的通知中心清爽了很多“找不到重要通知”的抱怨反而变少了。这个经历让我意识到系统通知的设计是一个“重要性与打扰性”的平衡游戏。权限、代码、参数都是手段真正的产品决策在于什么时候该弹、什么时候该息声。6. 我的最终取舍与使用建议写到这里该总结一下我的实际使用体感了。Notification API从代码量上看确实不大核心就几步判断支持、申请权限、构造通知、处理点击事件。但把它放到真实业务里决定成败的反而是那些“代码之外”的东西产品层面对通知频率和分级的定义、对用户权限被拒后的引导、对不同操作系统行为的适配预期。如果要给后来者一些经验我会说——如果只是做“页面打开时的站内提醒增强”不需要Service Worker直接在页面里用new Notification()就够了工作量小很多代码也直观。——如果目标是“页面关闭了也要收到提醒”就必须用Service Worker而且要提前规划好定时轮询的频率。频率太高会让服务端接口压力变大频率太低则提醒不够“即时”。在我这个工单系统里5分钟轮询是实际平衡下来的结果你可以根据自己的业务量调整。——通知文案务必短主体信息放前面。用户扫一眼系统通知的时间可能只有一两秒写得越精炼转化率越高。——权限申请按钮不要一上来就放在首屏正中央。我后来是放在“设置中心”里的配合一个简短的说明文案“开启后新工单会以系统通知形式提醒您”。这样的入口虽然多了一次用户点击但授权通过率反而比启动时弹窗高得多因为用户是在有明确意向的时候才去点它。——最后记得在本地准备一个能模拟denied、default、granted三种权限状态的调试方案。Chrome DevTools的感知模拟面板可以设置通知权限实测非常方便不用靠刷新页面猜测权限状态。这套方案跑下来工单的“触达率”比以前纯站内红点提升了不少。特别是那个“标签页切到别处也能收到提醒”的体验研发同学自己都直呼“终于有点C端产品的感觉了”。这样一个看起来简简单单的API背后涉及的安全上下文、权限机制、后台生命周期和系统行为差异才是真正决定项目成败的深水区。把每个环节都摸清楚之后你会觉得浏览器系统通知这件事确实没有想象中那么神秘但也没有文档里写的那么轻松。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑