使用 @novu/react 为 React 应用集成 Novu Inbox 通知中心:从 Keyless 快速体验到 HMAC 安全加固
【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu
@novu/react是 Novu 官方发布的 React SDK,核心交付物是一个开箱即用的<Inbox />通知中心组件,可在几分钟内为 React 应用接入完整的应用内通知能力(未读角标、通知列表、实时推送、偏好设置等)。本文以 packages/react/README.md 为主线,结合仓库源码,系统讲解安装、Keyless 快速试用、真实订阅者接入、自定义后端地址、受控开关、本地化以及 HMAC 加密安全机制,帮助你快速落地一个生产可用的应用内通知中心。
安装与包结构
在 React 应用中安装@novu/react仅需一条命令:
npm install @novu/react从 packages/react/package.json 可以看到,该包当前版本为3.19.2,其 peerDependencies 要求react与react-dom为^18.0.0 || ^19.0.0 || ^19.0.0-0(react-dom为可选),底层唯一的核心依赖是@novu/js(workspace 引用)。包通过exports字段提供了多个子路径入口:
@novu/react(默认入口,浏览器端 ESM/CJS)@novu/react/hooks——仅引入 Hooks,适合自定义 UI 场景@novu/react/themes——主题变量@novu/react/server——服务端渲染相关入口@novu/react/internal——内部 API
从 packages/react/src/index.ts 的导出清单看,SDK 除Inbox外还导出Bell、Notifications、Preferences、NovuProvider等组件,以及useNotifications、useCounts、usePreferences、useSubscription等 Hooks,本文聚焦 README 主线中的<Inbox />用法。
Keyless 模式:零配置秒级体验
Keyless 模式专为本地测试与快速实验设计:无需任何环境配置,直接引入并渲染<Inbox />即可看到完整的通知中心界面。
import React from 'react'; import { Inbox } from '@novu/react'; export function App() { return <Inbox />; }这一模式在源码中有明确的实现支撑。查看 Inbox.tsx 可以看到注释:"for keyless we provide an empty string, the api will generate a identifier"——当未传入applicationIdentifier时,SDK 会以空字符串占位;在 session.ts 中,会话初始化后若服务端返回以pk_keyless_前缀开头的应用标识符,SDK 会将其存入localStorage并在后续会话中复用,从而实现"免配置"的持续体验。对应地,types.ts 中subscriber、subscriberId、applicationIdentifier三者均可缺省的类型分支就是 Keyless 模式。
连接真实订阅者:applicationIdentifier 与 subscriber
要把通知中心接入你的 Novu 环境和真实用户,需要传入两个关键属性:
applicationIdentifier:应用标识符,相当于 API 通信的"公钥",可在 Novu Dashboard 的 API Keys 页面获取;subscriber:订阅者 ID,即你业务系统中用户的唯一标识(通常是数据库中的用户 ID)。
import { Inbox } from '@novu/react'; function Novu() { return ( <Inbox subscriber='SUBSCRIBER_ID' applicationIdentifier='APPLICATION_IDENTIFIER' /> ); }需要注意,subscriber与subscriberId在类型上是互斥的:types.ts 中subscriberId被标记为@deprecated(兼容历史版本,见 NV-5801),推荐统一使用subscriberprop。在 Inbox.tsx 中,SDK 会通过buildSubscriber将两者归一化为标准Subscriber对象,再交给InternalNovuProvider创建Novu实例,最终在session.initialize阶段带上subscriberId与浏览器时区(见 session.ts)与服务端建立会话。
自定义后端与 Socket 地址
默认情况下,SDK 使用 Novu 托管的 API 与 Socket 服务。在自托管(self-hosted)场景中,可以通过backendUrl与socketUrl覆盖默认地址:
import { Inbox } from '@novu/react'; function Novu() { return ( <Inbox backendUrl='YOUR_BACKEND_URL' socketUrl='YOUR_SOCKET_URL' subscriber='SUBSCRIBER_ID' applicationIdentifier='APPLICATION_IDENTIFIER' /> ); }这两个属性连同socketOptions(如自定义重连策略)、apiUrl等一起被组装进NovuOptions(见 Inbox.tsx),在 NovuProvider.tsx 中传给底层new Novu({ ... })构造器。若你的 Novu 实例由上层NovuProvider统一提供,Inbox会通过useUnsafeNovu直接复用该实例,避免重复初始化。
受控 Inbox:用 open 属性管理弹层状态
默认的 Inbox 弹层(popover)由组件内部管理开关。如果希望完全掌控开关时机(例如在导航栏的按钮上控制),可以传入open属性实现受控模式:
import { Inbox } from '@novu/react'; function Novu() { const [open, setOpen] = useState(false); return ( <div> <Inbox subscriber='SUBSCRIBER_ID' applicationIdentifier='APPLICATION_IDENTIFIER' open={isOpen} /> <button onClick={() => setOpen(true)}>Open Inbox</button> <button onClick={() => setOpen(false)}>Close Inbox</button> </div> ); }open只是DefaultInboxProps中的可选布尔属性之一。结合 types.ts,<Inbox />还支持placement(弹层位置)与placementOffset(偏移量),以及一系列渲染回调:renderNotification(整体自定义通知项)、renderAvatar/renderSubject/renderBody(分别定制头像、标题与正文)、renderDefaultActions/renderCustomActions(默认/自定义操作按钮)、renderBell(自定义铃铛)和点击回调onNotificationClick、onPrimaryActionClick、onSecondaryActionClick。这些回调通过 Renderer.tsx 与 NovuUI.tsx 中的mountComponent机制注入到底层 UI 引擎,实现 React 渲染与声明式 UI 的双向桥接。
本地化:localization 属性
通过localization属性可以替换 Inbox 界面文案,支持任意语言:
import { Inbox } from '@novu/react'; function Novu() { return ( <Inbox subscriber='SUBSCRIBER_ID' applicationIdentifier='APPLICATION_IDENTIFIER' localization={{ 'inbox.status.archived': 'Archived', 'inbox.status.unread': 'Unread', 'inbox.status.options.archived': 'Archived', 'inbox.status.options.unread': 'Unread', 'inbox.status.options.unreadRead': 'Unread/Read', 'inbox.status.unreadRead': 'Unread/Read', 'inbox.title': 'Inbox', 'notifications.emptyNotice': 'No notifications', locale: 'en-US', }} /> ); }localization的类型InboxLocalization由@novu/js/ui定义并从 index.ts 重新导出,覆盖通知列表状态(未读/已读/归档)、标题、空状态提示、操作项等多个文案键;locale键则用于指定日期等本地化格式。运行时,该对象会在 NovuUI.tsx 中通过novuUI.updateLocalization(options.localization)动态下发给 UI 引擎,无需重新挂载组件即可生效。
HMAC 加密:防止订阅者身份伪造
为什么需要 HMAC
applicationIdentifier是公开的"公钥",任何拿到它的人都可以直接调用 API。如果攻击者把请求中的subscriberId换成其他用户的 ID,就能越权读取他人的通知流。HMAC(Hash-Based Message Authentication Codes)加密用你的**服务端密钥(API Key)**对subscriberId进行签名,使服务端能够校验该订阅者 ID 是否由你的后端合法签发,从而杜绝恶意用户冒充他人。
启用 HMAC 加密
在 Novu 管理后台的In-App(应用内)设置页面,为当前环境开启 HMAC encryption 即可。
订阅者 HMAC:服务端生成、客户端携带
在服务端(Node.js)使用密钥对subscriberId生成 SHA-256 HMAC 十六进制摘要:
import { createHmac } from 'crypto'; const subscriberHash = createHmac('sha256', process.env.NOVU_API_KEY).update(subscriberId).digest('hex');然后将明文subscriberId与哈希值一并传给客户端组件:
<Inbox subscriber={'SUBSCRIBER_ID_PLAIN_VALUE'} subscriberHash={'SUBSCRIBER_ID_HASH_VALUE'} applicationIdentifier={'APPLICATION_IDENTIFIER'} />注意:如果 In-App 提供商设置中已启用 HMAC 加密,而
subscriberHash与subscriberId未同时提供,Inbox 将无法加载。
从实现上看,subscriberHash会一路传递到会话初始化:在 session.ts 中,subscriberHash与contextHash被原样送入initializeSession请求体,由服务端用密钥重新计算比对,校验通过后才建立会话。
Context HMAC:保护附加数据(可选)
如果通过context属性传递附加数据(如租户信息、环境标识等),攻击者同样可能篡改这些数据,因此需要额外生成contextHash:
import { createHmac } from 'crypto'; import { canonicalize } from '@tufjs/canonical-json'; const context = { tenant: 'acme', app: 'dashboard' }; const contextHash = createHmac('sha256', process.env.NOVU_API_KEY) .update(canonicalize(context)) .digest('hex');把context与contextHash一起传给组件:
<Inbox subscriber={'SUBSCRIBER_ID_PLAIN_VALUE'} subscriberHash={'SUBSCRIBER_ID_HASH_VALUE'} context={{ tenant: 'acme', app: 'dashboard' }} contextHash={'CONTEXT_HASH_VALUE'} applicationIdentifier={'APPLICATION_IDENTIFIER'} />注意:启用 HMAC 加密且提供了
context时,contextHash为必填。由于使用@tufjs/canonical-json做规范化序列化,哈希与键的顺序无关——{a:1, b:2}与{b:2, a:1}会生成相同的哈希,服务端与客户端按同一规则计算即可稳定比对。
在 types.ts 中,subscriberHash与contextHash均为可选字符串属性;二者在 Inbox.tsx 中进入providerProps,最终成为NovuOptions的一部分。SDK 侧并不自行计算哈希——签名计算必须在持有密钥的服务端完成,密钥绝不可下发到浏览器端。
进阶:从组件到 Hooks 的自由度
README 的核心是<Inbox />开箱即用组件;如果需要完全自研 UI,@novu/react/hooks子路径提供了数据层 Hooks。例如 useNotifications.ts 支持按tags、read、archived、seen、severity、createdGte/createdLte等条件过滤拉取通知,返回notifications、isLoading、hasMore,以及readAll、seenAll、archiveAll、archiveAllRead、refetch、fetchMore等操作;useCounts.ts 则通过filters数组统计各类未读/未读数。两者都默认通过 WebSocket 监听notifications.notification_received、notifications.unread_count_changed等事件实时更新,也可传入realtime: false关闭内置实时订阅、改用refetch()自行驱动(对应NovuProvider的realtime配置,见 NovuProvider.tsx)。这正是 docs/platform/quickstart/react.mdx 中"10 分钟接入 React 实时应用内通知"能力背后的数据层基础。
小结
@novu/react的<Inbox />用最少的代码覆盖了通知中心的完整闭环:Keyless 模式用于快速验证,applicationIdentifier+subscriber接入真实用户,backendUrl/socketUrl适配自托管部署,open属性实现受控弹层,localization完成多语言,HMAC 体系(subscriberHash+contextHash)则从身份与数据两个维度堵住越权与篡改风险。若默认 UI 无法满足定制需求,还可借助render*回调与/hooks子路径逐步替换为完全自研的界面。
【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考