资讯动态

浏览器通知开发指南:Notification API 权限与Service Worker后台提醒实现

发布时间:2026/10/18 3:52:13 来源:尧图企业网站定制
1. 先搞清楚一件事浏览器通知到底能帮你解决什么问题我最近在做某个内部运营系统的前端改造时遇到一个很实际的痛点用户在后台提交一批数据后处理任务耗时不短这期间用户又不能一直盯着页面看。跑去看别的页面吧怕错过了结果一直等着吧又浪费时间。后来我接入了浏览器通知功能把任务完成的事件推给用户问题一下就解决了——用户在系统里点一下运行然后该干嘛干嘛去任务跑完浏览器弹一条通知点一下直接回到结果页。这个场景其实只是冰山一角。浏览器通知Web Notification本质上是一条从网页发到操作系统原生通知中心的提醒消息哪怕用户把浏览器切到后台甚至最小化只要浏览器进程还开着通知就能正常弹出来。写博客管理系统时可以通知文章发布完成做电商后台时可以通知订单状态变更做协作工具时可以通知有人了你做会议系统时可以通知会议即将开始。它的价值不在于替代站内信而在于把需要用户知晓的信息从网页内部推到用户当前正在关注的操作系统层面触达效率比页面内的弹窗高得多。这篇文章我会从权限机制讲起到基础Notification API用法再到Service Worker配合实现后台提醒最后把我实际开发中踩过的坑和调试方法一起列出来。适合正在做企业级前端项目、需要实现消息触达但还没找到合适方案的开发者也适合刚学会fetch和异步操作、想做一个有点成就感的前端小功能的同学照着抄。整个过程不依赖任何第三方SDK原生API就能搞定。需要先说清楚一个核心认知浏览器通知本身并不复杂复杂的是权限策略和用户心理。如果你在错误的时间请求权限用户点了阻止后续所有通知逻辑都会失效而且浏览器不会轻易再给你第二次弹窗机会。所以这篇文章虽然叫手把手实现但实际上我更想帮你把权限这件事想明白再动手写代码这样才不会白白浪费Notification API的能力。2. 权限模型浏览器为什么总是问你允许/阻止2.1 通知权限的本质是推送通道的开关浏览器把通知能力设计成一个权限通道而不是页面内部变量。你通过Notification.requestPermission()获取用户授权后浏览器会把你这个站点的通知能力登记到操作系统的通知中心。这意味着通知的展示行为由操作系统接管前端开发者只能控制发什么内容不能控制用户是否接收。这个权限一旦被拒绝情况比较麻烦——不是简单的if判断就能解决的。浏览器对权限状态的记忆是持久化的用户在当前域名下点了阻止下次刷新页面时权限状态仍然是denied你再次调用requestPermission()也不会弹出授权询问框而是直接返回denied。从产品角度讲这意味着你必须在用户第一次访问时就设计好权限请求的触发时机否则后面想再要这个权限只能引导用户去浏览器的站点设置里手动改。我在项目里常用的处理方式是这样的不在一进页面就请求权限而是等用户做了一个与通知强相关的操作后再请求。比如用户勾选了开启消息提醒这个开关或者主动点击了一个订阅通知按钮这时候请求权限的成功率会高很多。用户在操作系统的权限弹窗里看到的是xxx网站想给您发送通知如果此时用户正在明确表达我想接收通知的意愿他对这句话的接受度远高于刚打开页面就被突然问到。2.2 权限状态的三种取值与判断逻辑Notification.permission属性返回三个字符串值状态含义能否再次触发弹窗default用户尚未做出选择也就是未询问过可以调用requestPermission()会弹窗granted用户已允许不需要再请求直接new Notification()即可denied用户已拒绝不能再次弹窗必须引导用户去浏览器设置里手动改开发中容易忽视的一点是页面加载完成时Notification.permission的值和你浏览器里真实的状态不一定同步。在部分浏览器里如果用户在地址栏左侧的站点权限面板手动改过设置Notification.permission需要刷新页面后才会更新。所以如果你在做权限状态UI展示建议在请求权限后主动同步刷新一次页面上的状态标识而不是只依赖页面初始化时读取的那一次。权限判断的代码可以封装成一个工具函数保证项目里不同模块复用同一套逻辑async function requestNotificationPermission() { if (!(Notification in window)) { console.warn(当前浏览器不支持Notification API); return unsupported; } // 已经授权就直接返回 if (Notification.permission granted) { return granted; } // 已经拒绝就直接返回不要再次请求 if (Notification.permission denied) { return denied; } // default状态调用浏览器弹窗 const result await Notification.requestPermission(); return result; }调用方式很简单比如在用户点击开启通知按钮时触发const btn document.getElementById(enableNotifyBtn); let permissionState; btn.addEventListener(click, async () { permissionState await requestNotificationPermission(); if (permissionState granted) { new Notification(通知已开启, { body: 以后有新的任务结果会第一时间通知你 }); } else if (permissionState denied) { alert(你已拒绝通知权限可以在浏览器地址栏左侧的站点设置中重新开启); } });这里有个很多人踩过的细节requestPermission()在较新的浏览器里返回的是一个Promise可以用await接收结果。但老一些的版本比如2018年前的Chrome需要传回调函数。如果你要兼容老版本浏览器建议做一次能力检测let result; if (typeof Notification.requestPermission ! function) { result await new Promise((resolve) { Notification.requestPermission(resolve); }); } else { result await Notification.requestPermission(); }不过在2024年做新项目这种兼容处理基本可以忽略旧浏览器本身就已经不符合系统对现代Web应用的要求了。我更推荐直接按标准写法来省得代码里堆一堆历史遗留的兼容分支。3. 基础版实现用Notification构造函数发出第一条通知3.1 new Notification的参数与options详解权限拿到之后创建通知的代码本身并不复杂const notification new Notification(这是一个标题, { body: 这是通知的具体内容, icon: /static/icon.png, image: /static/banner.png, badge: /static/badge.png, tag: order-finished, renotify: true, requireInteraction: true, silent: false, });options里的每个参数都有它存在的意义挑几个影响最大的说一下。tag是一个容易被忽略但非常实用的参数。它是通知的唯一标识当你有两条tag相同的通知时后发的通知会替换掉前一条而不是在系统通知栏里堆叠出很多条。这在业务场景里非常有用——比如一个订单的状态从处理中变为已完成如果你分别发了两条通知用户的通知栏里会出现两条信息显得很杂乱。设置同一个tag之后旧通知被替换用户只需要看最新一条。但如果你的业务需要同时展示多条独立通知比如聊天应用里多个会话就不要设置tag或者给每个会话设置独立的tag。我见过有人图省事所有通知都统一写成tag: message结果用户同时开着两个聊天窗口后到的消息把前一条覆盖了完全没法用。requireInteraction控制的是通知自动消失的问题。默认情况下操作系统的通知几秒后会自动收进通知中心如果用户没注意到就错过了。把requireInteraction设为true通知会一直停留在屏幕上直到用户点击或手动关闭。这种模式适合任务提醒、预约提醒等必须看到的场景。缺点是可能打扰用户频繁弹出且不消失的通知体验很糟糕所以要控制频率。vibrate参数可以控制移动端通知到来时的振动模式比如vibrate: [200, 100, 200]代表振动200ms、停顿100ms、再振动200ms。桌面端浏览器基本不支持这个属性但它不影响桌面端的正常展示所以移动端项目可以直接写上。3.2 一个完整的Demo用户操作后立刻给出反馈下面给一个可以直接复制到项目里的基础示例场景是用户提交了一条工单系统通知提交成功并附带工单编号。这个案例覆盖了从按钮点击到权限请求再到通知展示的完整链路。HTML结构button idsubmitTicket提交工单/buttonJavaScript逻辑const submitBtn document.getElementById(submitTicket); submitBtn.addEventListener(click, async () { // 模拟提交请求 const ticketNo TK20241001- Date.now(); // 请求权限如果之前已经granted这里会直接返回 const permission await requestNotificationPermission(); if (permission granted) { new Notification(工单提交成功, { body: 您的工单编号为 ${ticketNo}我们将在1个工作日内处理。, tag: ticket-${ticketNo}, requireInteraction: false, silent: false, }); } // 不管权限如何页面内都要有反馈 alert(工单 ${ticketNo} 提交成功); });在开发环境调试时有个重要判断localhost和http协议的站点在权限请求上是有豁免的。Chrome对localhost环境默认不限制通知权限所以你在本机测试时权限弹窗可能会特别顺利但部署到线上HTTPS环境时用户仍然会看到一次权限询问。这一点需要提前跟产品沟通上线后用户第一次操作时出现权限弹窗是正常行为不要以为是Bug。另一个值得注意的点是通知创建的时机。浏览器不允许在用户切到后台时随意创建通知但允许在任意时刻创建Notification对象。真正需要注意的是代码里不应该在页面初始化的循环里同步创建几十条通知操作系统会把这当成骚扰行为部分浏览器会自动折叠多余通知甚至忽略它们。通知应该是事件驱动的每次创建一条不要批量轰炸。4. 让通知真正好用点击行为、图标设置与多开管理4.1 点击通知后的行为控制如果用户只看到通知、点一下却没有任何反应这个功能的价值就少了一大半。通知的核心交互就是点击——点击后要跳转到对应的页面并需要把焦点带回浏览器窗口。点击行为通过监听Notification实例的click事件来实现const notification new Notification(订单已发货, { body: 点击查看物流详情, tag: order-shipped, }); notification.onclick () { // 先把焦点切到当前窗口 window.focus(); // 再跳转到对应路由 window.location.href /orders/detail?id12345; // 关闭这条通知避免残留 notification.close(); };注意这里必须写window.focus()。原因是用户在通知栏点通知时浏览器窗口可能已经在后台如果不主动聚焦页面即使跳转了用户看到的还是之前的页面需要手动点一下浏览器窗口才能注意到变化。这个细节线上反馈最好——用户以为点击没生效其实只是焦点没切过来。有些项目用的是单页应用SPA路由由前端控制。这时候点击通知的跳转逻辑要跟路由体系配合。如果项目用的是Vue Router或React Router你需要拿到router实例再跳转而通知回调里的this指向上下文并不是组件实例所以最好把跳转封装成一个全局函数或者把通知点击处理放在一个独立的模块里// notify.js import router from ../router; export function notifyAndNavigate(title, options, routePath) { if (Notification.permission ! granted) return; const notification new Notification(title, options); notification.onclick () { window.focus(); router.push(routePath); notification.close(); }; }这样组件里只需要调用一个函数路由跳转逻辑统一维护不会出现页面改版了但通知跳的还是旧地址的问题。4.2 图标和排版在不同平台的差异icon和badge这两个参数的设计意图不同。icon是通知主界面的图标尺寸建议96x96像素以上badge是折叠状态下的角标图标一般出现在系统状态栏或通知栏侧边尺寸建议18x18像素最好用单色透明背景的图标。你可以不区分这两个参数直接用同一个图标但视觉上在部分系统里会比较粗糙。另外Chrome和Windows的通知支持image参数在展开的通知卡片里显示一张大图macOS的Safari不支持image只显示icon。如果你的业务里需要图文通知建议把关键信息放在body文字里图片只作为视觉增强不要依赖图片传达核心信息。移动端浏览器Android Chrome、iOS Safari的通知展示规则和桌面端不完全一样。iOS Safari从iOS 16.4开始才支持Web Push通知在较老版本的iOS上Web App需要添加到主屏幕后才能接收通知普通Safari里的网页无法调用new Notification()。Android Chrome对通知的支持比较完善但系统有自己的通知分组和推送渠道管理规则前端能控制的空间比桌面端少。做移动端适配时我的原则是核心能力只依赖标准的title、body、icon、tag其余参数都视为渐进增强能生效最好不生效也不影响主流程。5. 从页面通知到后台通知Service Worker在通知链路里的角色5.1 为什么只在页面里new Notification还不够前面写的代码有一个明显缺陷通知的生命周期绑定在页面标签页上。如果用户把标签页关了页面的JavaScript上下文就销毁了指向Notification对象的引用也丢失通知自然发不出来。即便只是刷新页面前一秒还挂在内存里的通知也会消失。要解决用户关掉页面后还能收到提醒这个问题就需要Service Worker介入。Service Worker是浏览器独立于页面运行的脚本它有自己的生命周期即使页面关闭只要浏览器进程还活着Service Worker仍然可以执行部分逻辑。通知功能和Service Worker结合有两种主流方式第一种是纯前端模拟的后台检查方案Service Worker里定时去请求后端接口如果发现状态变更就通过self.registration.showNotification()弹出通知。这个方案不需要服务器推送适合中小型项目中列表页关掉了但还想收到结果的场景。第二种是Web Push协议方案服务器通过Push Service如浏览器厂商提供的推送服务向Service Worker推送消息。这种方式省电省流量是正规的推送方案但需要后端配合维护订阅关系和推送密钥改造工作量比较大。对大部分人来说从第一种方案入手更容易看到效果、理解整个机制。下面重点展开第一种。5.2 注册Service Worker并创建后台任务提醒第一步在项目里新建一个service-worker.js文件放在静态目录下确保它能被浏览器直接访问到。在页面里注册它if (serviceWorker in navigator) { navigator.serviceWorker.register(/service-worker.js) .then((registration) { console.log(Service Worker 注册成功, registration.scope); }) .catch((err) { console.error(Service Worker 注册失败, err); }); }Service Worker内容self.addEventListener(install, (event) { // 让新的SW立即激活不等待旧页面关闭 self.skipWaiting(); }); self.addEventListener(activate, (event) { event.waitUntil(self.clients.claim()); }); // 监听页面发来的消息页面关闭后SW依然可以响应定时任务 self.addEventListener(message, (event) { const { type, code } event.data || {}; if (type start-task) { startMonitoring(code); } }); let monitoringTimer null; function startMonitoring(taskCode) { // 先清掉旧定时器防止重复开启 if (monitoringTimer) { clearInterval(monitoringTimer); monitoringTimer null; } monitoringTimer setInterval(async () { try { // 请求后端接口查询任务状态 const response await fetch(/api/task/status/${taskCode}); const data await response.json(); if (data.status finished) { // 任务完成弹通知 self.registration.showNotification(任务已完成, { body: 任务 ${taskCode} 处理完成点击查看结果, icon: /static/icon.png, tag: task-${taskCode}, requireInteraction: true, data: { taskCode } }); // 通知已弹出停止轮询避免重复提醒 clearInterval(monitoringTimer); monitoringTimer null; } } catch (err) { // 网络异常时不弹通知等下一次轮询重试 console.error(轮询任务失败, err); } }, 10000); // 每10秒检查一次 }页面端启动后台任务的代码async function startBackgroundTask(taskCode) { // 请求权限 await requestNotificationPermission(); // 确保SW已经注册成功 const registration await navigator.serviceWorker.ready; // 向SW发送消息 registration.active.postMessage({ type: start-task, code: taskCode }); }这个方案的取舍点在于setInterval在Service Worker里执行时不能100%保证每10秒唤醒一次——浏览器为了省电会对后台SW做节流尤其是移动端Chrome。但相比页面里的setIntervalSW方案的优势是页面关闭后定时器仍然有效。如果业务对实时性要求很高比如秒级纯前端轮询就不合适需要上Web Push。如果只是任务跑完了告诉我一声轮询完全够用。上面例子里的data字段值得留意showNotification()的options.data里可以携带任意自定义数据点击通知时通过notification.data取出来。这在通知点击跳转时非常有用——不用在代码里硬编码路由直接在通知数据里带上跳转所需的信息self.addEventListener(notificationclick, (event) { const { taskCode } event.notification.data; event.notification.close(); // 打开/聚焦已存在的页面 event.waitUntil( self.clients.matchAll({ type: window, includeUncontrolled: true }) .then((clientList) { for (const client of clientList) { if (client.url.includes(/tasks) focus in client) { return client.focus(); } } // 如果当前没有对应页面就新开一个 return self.clients.openWindow(/tasks/detail?code${taskCode}); }) ); });注意notificationclick事件的触发上下文不在页面里而SW本身没有window对象所以焦点处理和页面内点击不一样需要借助clients.matchAll查找已存在的窗口。这里的includeUncontrolled: true是把那些没有被当前SW控制的页面也纳入查找范围防止明明页面开着却找不到导致重复开窗。6. 调试与踩坑权限被拒、通知不显示、tag覆盖与移动端差异6.1 权限被拒后的完整恢复路径我在一个实际项目里遇到过这样的情况测试同学在QA环境第一次访问页面时手滑点了阻止之后所有测试用例都因为通知收不到而失败而且刷新页面、重新调用requestPermission()都没用因为浏览器已经把该站点的权限记录为denied了。排查很久才意识到问题出在权限记忆上。恢复路径在桌面上相对简单地址栏左侧的站点权限图标点开把通知改为允许然后刷新页面。这个操作对用户要求不高但要主动告诉用户怎么做。所以我在项目里做了一套权限被拒后的引导UI当检测到Notification.permission denied时页面右下角出现一个不显眼的横幅内容是您已关闭通知权限如需接收提醒请点击这里修改设置点击后展开一步操作指南用箭头标注出浏览器地址栏里应该点哪里。移动端浏览器的权限恢复略繁琐不同浏览器入口不同。Android Chrome是设置→站点设置→通知iOS Safari如果站点没有添加到主屏幕只能长按站点图标修改。如果你的目标用户大量使用移动端建议产品层面不要在恢复权限这件事上做太重的引导因为路径实在太深用户容易中途放弃。更务实的做法是提供站内消息中心作为通知的下位替代——通知没打开用户在页面里也能看到历史消息列表。6.2 调试面板与常见的通知消失问题Chrome DevTools里可以模拟通知权限状态、手动触发通知事件方便在没有真实订阅的情况下验证逻辑。打开 DevTools切到Application面板左边找到Storage下的Notification可以看到当前站点的权限状态。也可以直接在Console里操作// 模拟授予权限 Notification.requestPermission () Promise.resolve(granted);有个很常见的通知发了但没弹出来的问题是当前浏览器窗口处于前台聚焦状态时部分操作系统会静默抑制通知。比如你在Windows上正盯着Chrome看网页后台突然弹一条通知有的系统设置会把它隐藏掉只在通知中心里保留一条记录。这很容易让人误以为是代码Bug其实操作系统行为。测试通知时建议把浏览器窗口切到后台或者把页面切到另一个虚拟桌面这样通知弹不弹一目了然。另一个问题是tag覆盖导致的通知好像丢了。设想用户的一条任务列表里有10个任务同时完成你循环发10条通知每条都用了tag: task——对不起最终用户只会看到最后一条前9条全部被替换。如果你需要所有任务的结果都展示比如任务A完成、任务B完成、任务C完成分开列出就要用不同的tag或者在一条通知的body里汇总所有任务名。从信息传达的效率看批量完成的情况更适合汇总成一条通知而不是分别弹10条后者也是对系统通知栏的打扰。6.3 实战经验关于轮询间隔与用户打扰控制的个人观点再回到第5章的方案说个真实场景里的细节。刚开始我写的轮询间隔是5秒因为觉得任务状态变化挺频繁希望能尽快通知用户。后来发现后台接口在无人访问时本身没有压力但用户收到通知后在反馈里抱怨提醒太频繁了。分析后发现任务的实际处理是分批的同一时间会有一批任务一起完成5秒的轮询会让通知在极短时间内连续弹出用户当然烦。后来我把轮询间隔改成30秒并且在通知逻辑上加了一个会话级通知合并同一个批次的任务完成时先缓存任务结果等到轮询下一次跑的时候一起弹一条通知。体验比之前好了很多用户反馈里再没有提过烦人。所以我的建议是轮询间隔不是越短越好要和业务对实时性的真实需求匹配。秒级触达的需求适合Web Push纯前端轮询的话设计上多关注通知频率和通知整合这两个点比单纯追求弹得快更靠谱。调试期间还有个经验Service Worker更新后的缓存问题也容易坑人。你改了service-worker.js里的轮询逻辑但线上运行的一直是老版本。这要么是注册的SW在install事件里主动缓存了自己要么是页面重新加载时浏览器还在用旧SW。开发阶段直接在install事件里调用self.skipWaiting()activate里调用self.clients.claim()能缓解大部分问题。上线前记得把不带缓存的更新逻辑检查一遍否则出现线上用的还是旧逻辑会让排查变得非常痛苦。7. 最后再分享两个我常用的通知设计技巧先说第一个。Notification API的silent参数很实用——设为true时通知弹出但不响铃振动。我一般会在通知里附带一个来源类型的判断如果通知内容是高优先级比如会议即将开始silent: false如果是普通内容比如周报已生成silent: true。同样是通知用户体验的差异很大用户可以接受被重要的事吵到但绝不愿意为了周报已生成这种小事被打断。第二个技巧是通知权限请求的预演。在真正调用requestPermission()之前先用一个页面内自定义弹窗向用户解释接下来浏览器会问你要不要允许通知这个权限用来做什么你点允许以后会看到什么效果。等用户在页面内点下一步的时候再调用原生权限弹窗。这个两段式的流程在转化率上的提升非常明显原理也很简单原生权限弹窗本身没有解释功能用户突然看到xxx网站想要发送通知会本能地警惕但如果你提前用页面内的温和方式说明白他的安全警惕已经被解除自然更容易点允许。我在多个项目里试过直接把用户引到原生弹窗的请求成功率大约在三四成而做了预演解释之后能到六成以上。这不是什么高深的技术就是多考虑了一步用户心理收益却很直接。如果你的产品非常依赖通知触达这个预演流程一定要加上。

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

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

免费获取报价 →
↑