Firebase Cloud Messaging Web SDK 演进实录:从 FCM Token 到 FID 注册的新一代 API
Firebase Cloud Messaging Web SDK 演进实录:从 FCM Token 到 FID 注册的新一代 API
本篇文章以 @firebase/messaging 的版本变更日志(packages/messaging/CHANGELOG.md,覆盖 0.6.11 至 0.13.3)为核心脉络,系统梳理 Firebase Cloud Messaging Web SDK 在 API 形态、消息处理、遥测上报、兼容性与构建分发上的演进过程。通过结合仓库内真实源码(packages/messaging/src),你将理解新一代 FID(Firebase Installation ID)注册 API 与旧 FCM Token API 的实现差异、onMessage/onBackgroundMessage 的前后台消息链路、BigQuery 交付指标导出的队列机制,以及 SDK 在浏览器兼容与打包分发上的取舍。读完本文,你能依据版本演进路线快速定位自己的项目应该使用哪一套 API,并能读懂底层源码的关键调用链。
一、版本演进总览:一条清晰的技术路线图
从 CHANGELOG 可以还原出 @firebase/messaging 的三大演进阶段:
- 传统 FCM Token 时代(0.6.x ~ 0.7.0):以
getToken()获取 FCM registration token 为核心,通过setBackgroundMessageHandler、onTokenRefresh、useVapidKey、useServiceWorker等前置配置方法使用。 - 模块化 API 转型期(0.7.0 ~ 0.12.x):引入面向页面与 Service Worker 的模块化接口
getToken(options)、onMessage、onBackgroundMessage,补齐MessagePayload、NotificationPayload、FcmOptions类型,并持续修复 Token 生命周期、isSupported()检测、消息 ID 解析等实际问题。 - FID 注册新范式(0.13.0 ~ 0.13.3):新增
register/unregister/onRegistered/onUnregistered四件套,将注册标识从"FCM Token"迁移到"Firebase Installation ID(FID)",并显著优化了 BigQuery 交付指标导出的实时性。
各版本的依赖坐标在 packages/messaging/package.json 中可见:当前版本为 0.13.3,运行依赖为 @firebase/installations@0.6.24、@firebase/messaging-interop-types@0.2.6、@firebase/util@1.15.3、@firebase/component@0.7.5、idb@7.1.1 与 tslib,并以 @firebase/app(0.x)作为 peer dependency。
二、0.13.0 里程碑:FID 注册 API 与旧 Token API 的交接
0.13.0 是 CHANGELOG 中最具分量的一个版本(Minor Changes),它同时做了两件事:新增 FID 注册 API 并弃用旧 Token API,以及修复 BigQuery 指标导出的 Flush 时机。
2.1 新增的四件套 API
在 packages/messaging/src/api.ts 中可以看到这套新 API 的公开签名:
| API | 签名 | 职责 |
|---|---|---|
register |
register(messaging, options?): Promise<void> |
用 FID 建立注册身份,FID 通过 onRegistered 回调交付,而非返回值 |
unregister |
unregister(messaging): Promise<void> |
删除 FID 注册,成功时触发 onUnregistered |
onRegistered |
onRegistered(messaging, nextOrObserver): Unsubscribe |
监听注册成功事件,拿到 FID 后上传到应用后端 |
onUnregistered |
onUnregistered(messaging, nextOrObserver): Unsubscribe |
监听注销事件,通知后端移除 FID,避免发送时出现 404 |
同时被标记为 @deprecated 的旧 API 包括:getToken、deleteToken、setBackgroundMessageHandler、onTokenRefresh、useVapidKey、useServiceWorker。CHANGELOG 明确指出:应"Use register together with onRegistered for Firebase Installation ID-based messaging instead of retrieving an FCM registration token"。
2.2 register() 的源码级调用链
register 的实际实现位于 packages/messaging/src/api/register.ts,其流程为:
- 环境检查:要求处于 Window 上下文(
navigator存在),否则抛AVAILABLE_IN_WINDOW; - 通知权限处理:
Notification.permission为default时先requestPermission(),最终不是granted则抛PERMISSION_BLOCKED; - 要求已注册
onRegistered回调,否则抛INVALID_ON_REGISTERED_HANDLER; - 依次执行
updateVapidKey与updateSwReg(与getToken相同的 options 处理,见 packages/messaging/src/helpers/updateVapidKey.ts,未显式传入时使用默认 VAPID key); - 将注册过程串入
messaging._registerNotifyChain队列,避免并发register()竞争; - 通过
installations.getId()取得 FID,判断是否需要刷新注册(FID_REGISTRATION_REFRESH_MS = 7 * 24 * 60 * 60 * 1000,即 7 天周期,或 FID 变化、无本地记录时); - 调用
registerFcmRegistrationWithFid(packages/messaging/src/internals/register-fid.ts)向后端CreateRegistration接口提交 push 订阅信息,并要求响应中的name(projects/{projectId}/registrations/{fid})回显的 FID 与installations.getId()一致,最多重试 3 次(FID_REGISTRATION_FID_MATCH_MAX_ATTEMPTS); - 落库 FID 注册元数据后调用
notifyOnRegistered(messaging, fid),把 FID 交给用户回调。
对应地,unregister(packages/messaging/src/api/unregister.ts)优先读取本地存储的 FID,调用 requestDeleteRegistration 删除服务端注册,再做本地清理(同时尽力清理旧 Token 元数据),最后触发 onUnregisteredHandler。
2.3 与旧 getToken() 的关系:Token 管理内部机制
旧 API 仍然保留,但已不再推荐。getToken(packages/messaging/src/api/getToken.ts)的核心逻辑在 getTokenInternal(packages/messaging/src/internals/token-manager.ts):
- 无本地 Token →
getNewToken向后端订阅并写入 IndexedDB; - 订阅参数(vapidKey / endpoint / auth / p256dh)不匹配 → 先删除旧 Token 再重新获取(
isTokenValid逐个字段比较,见同文件 L220-L230); - 超过
TOKEN_EXPIRATION_MS(也是 7 天)→ 走requestUpdateToken的 PATCH 刷新; - 有效 → 直接返回本地 Token。
0.12.8 的 Patch 修复正是针对这个环节:"Revised token update logic to keep existing tokens during update failures, preventing unnecessary deletions for transient issues",即更新失败时保留既有 Token,避免瞬时网络问题导致误删。这在 updateToken 的实现中可以印证:只有刷新成功后才写入新的 TokenDetails(packages/messaging/src/internals/token-manager.ts)。
三、消息接收 API 的演进:onMessage 与 onBackgroundMessage
0.7.0 引入了模块化的 getToken(options: {serviceWorkerRegistration, vapidKey}) 与 onBackgroundMessage,同时为 MessagePayload、NotificationPayload、FcmOptions 补全了类型定义。
3.1 前后台消息分发链路
消息链路的核心在 Service Worker 侧。onPush(packages/messaging/src/listeners/sw-listeners.ts)处理逻辑如下:
- 解析 PushEvent 数据为
MessagePayloadInternal(JSON 解析失败即视为非 FCM 消息并跳过); - 若启用了 BigQuery 指标导出,则调用
stageLog上报交付事件; - 通过
self.clients.matchAll检查是否存在可见的 WindowClient(且排除chrome-extension://页面):- 前台:
postMessage把内部 payload 发往各页面窗口,由页面侧的messageEventListener(packages/messaging/src/listeners/window-listener.ts)触发onMessage回调; - 后台:若 payload 含
notification,用self.registration.showNotification展示通知,再把外部化后的 payload 交给onBackgroundMessage回调。
- 前台:
externalizePayload(packages/messaging/src/helpers/externalizePayload.ts)负责把内部 snake_case 字段(collapse_key、fcmMessageId、click_action、analytics_label)转换为对外暴露的 camelCase 公共类型 MessagePayload(from、collapseKey、messageId、notification、data、fcmOptions),类型定义见 packages/messaging/src/interfaces/public-types.ts。
3.2 0.8.0:messageId 进入公共载荷
0.8.0 的 Minor Change 是"Allows retrieval of messageId from MessagePayload"。在 externalizePayload 中即 messageId: internalPayload.fcmMessageId 一行(packages/messaging/src/helpers/externalizePayload.ts)。0.9.6 的 "Fix message id parsing" 则是对消息 ID 解析的进一步修补。
3.3 silent push 警告的两次修复(0.7.3 与 0.9.9)
- 0.7.3:为
onBackgroundMessage钩子增加超时,若在 1 秒内调用了showNotification就不会再显示 silent-push 警告; - 0.9.9:
Await on onBackgroundMessage,在回调返回 Promise 时正确 await,消除未处理 Promise 导致的静默警告。
这两次修复对应同一体验问题:Service Worker 收到静默推送后若迟迟不展示通知,浏览器会提示"此站点已在后台更新"类警告,SDK 通过等待回调完成来消除误报。
3.4 通知字段补齐:icon 与 link
- 0.10.0 / 0.11.0:连续两个 Minor 版本"Expose 'icon' field ... as part of the 'notification' payload",最终
NotificationPayload拥有title、body、image、icon四个字段; - 0.9.14:修复缺失的
FcmOptions.link。在externalizePayload中可以看到link的兜底逻辑:fcmOptions.link为空时回退到notification.click_action(packages/messaging/src/helpers/externalizePayload.ts)。点击通知时,onNotificationClick(packages/messaging/src/listeners/sw-listeners.ts)只会打开/聚焦与站点同源(url.host === originUrl.host)的链接,这是 0.7.1 "stops redirecting user to non-origin urls" 的延续。
四、BigQuery 交付指标导出:0.13.0 的 Flush 时机修复
0.12.26 起 CHANGELOG 密集出现与遥测相关的条目:
- 0.12.5:
Tag product id for internal big query metric logging(对应buildComplianceData(productId),见 packages/messaging/src/helpers/logToFirelog.ts); - 0.12.12:更新 logging endpoint,修复 BigQuery 日志缺失 WebPush 数据的
payload structure问题; - 0.13.0 Patch:重构 Firelog 上报调度——启用导出后立即在下一个 timer tick 首次 flush(
INITIAL_LOG_FLUSH_DELAY_MS = 0),不再等待完整的LOG_INTERVAL_IN_MS(86400000ms,即 24 小时);仅在队列中有事件时才启动处理,避免空队列时挂起一天的空转定时器阻塞后续stageLog;禁用导出时立即清空队列并取消待定定时器。
这些常量与逻辑都能在 packages/messaging/src/helpers/logToFirelog.ts 与 packages/messaging/src/util/constants.ts 中找到:
- 上报端点
https://play.google.com/log?format=json_proto3(packages/messaging/src/helpers/logToFirelog.ts); - 单请求最多 1000 条事件(
MAX_NUMBER_OF_EVENTS_PER_LOG_REQUEST)、最多重试 3 次(MAX_RETRIES)、失败退避默认 5000ms(DEFAULT_BACKOFF_TIME_MS); - 仅在 429/500/503/504 状态码下重试(
isRetriableError,同文件 L180-L189); - 每次收到 push 都会
stageLog入队并尝试启动上报服务(同文件 L191-L202),这正是 0.13.0 注释中"start processing only when there are queued events"与"staging a log starts the service when needed"的实现。
开关 API 为 experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messaging, enable)(packages/messaging/src/api/setDeliveryMetricsExportedToBigQueryEnabled.ts):启用时调用 startLoggingService,禁用时调用 stopLoggingServiceAndClearQueue。默认关闭(deliveryMetricsExportedToBigQueryEnabled = false,见 packages/messaging/src/messaging-service.ts)。
五、浏览器兼容性与运行环境:isSupported 与 IndexedDB
CHANGELOG 中多个 Patch 都与"在受限环境下正确失败"有关:
- 0.9.1:
checking isSupported led to runtime errors in certain environments; - 0.9.8:修复
isSupported()在环境不支持 IndexedDBopen()时产生 uncaught rejection(对应 #5868)。
当前实现(packages/messaging/src/api/isSupported.ts)对 Window 与 SW 两种上下文分别检测:
- Window 上下文要求:
serviceWorker in navigator、PushManager、Notification、fetch、ServiceWorkerRegistration.prototype.showNotification、PushSubscription.prototype.getKey、Cookie 可用、IndexedDB 可 open(Safari iframe 与 Firefox 隐私窗口可能受限,源码注释引用了 issue 2393); - SW 上下文要求:IndexedDB 可 open、
PushManager、Notification及上述两个原型方法。
IndexedDB 的依赖策略也几经变更:0.9.10 "Remove idb dependency and replace with our own code",0.9.13 又 "Replace stopgap firebase/util IndexedDB methods with idb library"(最终回到 idb)。旧版数据库迁移逻辑保留在 packages/messaging/src/helpers/migrate-old-database.ts,会把历史 fcm_token_details_db(V2~V4 schema)中的 Token 迁移到新版数据库;新版数据库 firebase-messaging-database(v2)包含 Token 与 FID 注册两个 object store,键为 appConfig.appId,见 packages/messaging/src/internals/idb-manager.ts。
六、Service Worker 注册时序:0.12.15 的关键修复
0.12.15 修复了 #7784:"PushManager.subscribe() is called too soon after registering the default service worker"。问题根源在于:MDN 文档声称 await navigator.serviceWorker.register() 后即可使用,但实际浏览器中注册状态可能仍是 installing/waiting,此时调用 pushManager.subscribe() 会报错。
修复实现位于 packages/messaging/src/helpers/registerDefaultSw.ts:registerDefaultSw 注册默认 SW(路径 /firebase-messaging-sw.js、scope /firebase-cloud-messaging-push-scope,常量见 packages/messaging/src/util/constants.ts)后调用 messaging.swRegistration.update() 强制更新,再通过 waitForRegistrationActive(同文件 L63-L92)等待 registration.active 或监听 onstatechange 进入 activated,并设 10 秒超时(DEFAULT_REGISTRATION_TIMEOUT),确保订阅发生在 SW 就绪之后。
七、构建目标与分发形态的演进
CHANGELOG 记录了三类工程化变更,直接影响用户如何引入 SDK:
- ES 版本下限:
- 0.12.13:移除 ES5 bundle,最低 ES2017;
- 0.12.23:构建目标升级到 ES2020;
- 0.12.0 / 0.10.0 / 0.12.14:TypeScript 依次升级到 4.7.4、5.5.4。
- CJS 与浏览器入口:
- 0.9.6:为
messaging/sw增加 CJS bundle,使 SSR 框架能在 Node 管线中正常加载; - 0.9.7:Node 环境下
exports始终指向 CJS bundle; - 0.12.2:增加 browser CJS 入口(供 Jest + JSDOM 模式使用)。
当前 packages/messaging/package.json 的
exports字段即为这些演进的结果:主入口区分types/require/module/default,./sw单独暴露 Service Worker 构建。
- 0.9.6:为
- exports 与类型声明:
- 0.9.3:所有包增加
exports字段; - 0.9.4:修复与
exports字段配合的构建脚本(对应 issue #1487); - 0.9.14:
add type declarations to exports field(对应 issue #6300); - 0.13.3:更新 api-extractor 与文档生成管线,同时修正若干文档注释与链接(这是 CHANGELOG 最新的一个版本)。
- 0.9.3:所有包增加
八、兼容性细节:VAPID Key 的编码往返
0.6.11 记录了一个典型的 Web Push 兼容性修复:FCM 曾切换到 base64 编码的 VAPID key 传给 PushManager,但为向后兼容又改回 ArrayBuffer(Uint8Array)。这一往返至今仍保留在代码中:getPushSubscription 调用 pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64ToArray(vapidKey) })(packages/messaging/src/internals/token-manager.ts),而 array-base64-translator.ts(packages/messaging/src/helpers/array-base64-translator.ts)负责 URL-safe base64 与字节数组的双向转换;请求体中则仅在 VAPID key 与默认值不同时才带上 applicationPubKey(packages/messaging/src/internals/requests.ts)。
九、错误码体系:排障速查
在 packages/messaging/src/util/errors.ts 中可以查阅 SDK 的全部错误码及消息模板,排障时重点关注:
| 错误码 | 触发场景 |
|---|---|
only-available-in-window / only-available-in-sw |
在错误的运行上下文调用对应 API |
permission-blocked / permission-default |
通知权限未授予 |
unsupported-browser / indexed-db-unsupported |
浏览器缺少必要 API(可先用 isSupported() 预检) |
failed-service-worker-registration |
默认 SW 注册失败(含 10 秒超时) |
fid-registration-failed / fid-unregister-failed |
FID 注册/注销失败(0.13.0 新增,含 CreateRegistration 回显 FID 不匹配) |
token-update-failed / token-unsubscribe-failed |
旧 Token 生命周期操作失败 |
invalid-on-registered-handler |
调用 register() 前未注册 onRegistered |
结语与下一步
从 0.6.11 到 0.13.3 的 CHANGELOG 完整呈现了一个生产级推送 SDK 的演进方法论:用新 API 取代旧 API 时保留兼容路径、针对真实浏览器行为反复修补时序问题、把遥测从"次日批量"优化到"启用即上报"、持续收紧构建目标以控制包体积。对于新项目,应直接采用 register/onRegistered/unregister/onUnregistered 这套 FID 注册范式;对于存量项目,可通过 getToken 与 deleteToken 的兼容实现平稳过渡(两者在 packages/messaging/src/api.ts 中仍可用,且 deleteToken 会同时处理旧 Token 与 FID 注册两种状态)。
想继续深入,可在仓库中依次阅读:packages/messaging/src/api.ts(公共 API 层)、packages/messaging/src/listeners/sw-listeners.ts(SW 事件处理)、packages/messaging/src/internals/token-manager.ts(Token/FID 状态机)、packages/messaging/src/internals/requests.ts(后端 HTTP 交互)以及对应的测试文件(如 token-manager.test.ts、sw-listeners.test.ts),以验证本文所述的调用链与行为。