ARTICLE DETAIL

资讯详情

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

Novu Inbox 个性化定制完全指南:Render Props、条件渲染、自定义弹层与多语言本地化

Novu Inbox 个性化定制完全指南:Render Props、条件渲染、自定义弹层与多语言本地化 Novu Inbox 个性化定制完全指南Render Props、条件渲染、自定义弹层与多语言本地化【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novuNovu 的开源 Inbox 组件novu/react可以在每一层进行个性化定制——从更换铃铛图标到逐行per-row自定义通知布局再到完全自定义的弹层与路由行为。本文基于 Novu 官方 Inbox 个性化参考文档结合仓库源码如 packages/js/src/ui/types.ts、packages/js/src/ui/components/Inbox.tsx、packages/js/src/ui/config/defaultLocalization.ts深入讲解 Render Props、点击处理、条件显示、HTML 内容、Inbox 原语组合、本地化与 Tabs 过滤读完即可把开箱即用的 Inbox 改造成与产品品牌、交互完全对齐的通知中心。Render Props在保留默认能力的前提下逐块替换 UIRender Props 允许你替换 Inbox 的特定部分同时保留外围的镀铬能力默认操作、悬停状态、动画、无障碍支持。从源码角度看这些渲染函数在 packages/js/src/ui/types.ts 中被定义为NotificationRenderer、AvatarRenderer、SubjectRenderer、BodyRenderer、DefaultActionsRenderer、CustomActionsRenderer、BellRenderer等类型每个函数都接收一个挂载元素和通知对象铃铛接收的是未读数返回一个清理函数。各 Render Prop 概览Prop替换的内容是否保留默认操作renderBell铃铛图标不适用N/ArenderAvatar通知头像✅ 保留renderSubject主题行✅ 保留renderBody正文文本✅ 保留renderDefaultActions标记已读 / 归档 / 稍后提醒按钮❌ 需要你自行实现renderCustomActions主操作 次操作按钮✅ 保留renderNotification整条通知行❌ 全部由你实现renderBell按严重程度展示未读角标renderBell接收按严重程度拆分的未读数unreadCount.severity类型为Recordstring, number见 packages/js/src/ui/types.ts你可以借此实现高危通知单独标红的角标逻辑import { Inbox, SeverityLevelEnum } from novu/react; Inbox applicationIdentifierYOUR_NOVU_APP_ID subscriberIdsubscriber-123 renderBell{(unreadCount) ( button classNamerelative p-2 BellIcon / {unreadCount.severity[SeverityLevelEnum.HIGH] 0 ( span classNameabsolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full px-1 {unreadCount.severity[SeverityLevelEnum.HIGH]} /span )} /button )} /SeverityLevelEnum定义于 packages/shared/src/consts/severity.ts包含HIGH、MEDIUM、LOW、NONE四个取值。源码层面packages/js/src/ui/components/elements/Bell/Bell.tsx 中Bell组件通过useUnreadCount()获取未读数若传入renderBell则走ExternalElementRenderer挂载你的自定义内容否则回退到默认的BellContainer。renderAvatar替换通知头像Inbox /* ... */ renderAvatar{(notification) ( img src{notification.avatar ?? /default-avatar.png} alt classNamew-8 h-8 rounded-full / )} /renderSubject/renderBody定制主题行与正文Inbox /* ... */ renderSubject{(notification) ( strong classNametext-sm{notification.subject}/strong )} renderBody{(notification) ( p classNametext-xs text-gray-600{notification.body}/p )} /renderDefaultActions自行实现标记已读 / 归档替换内置的已读 / 归档 / 稍后提醒交互后这些操作必须由你重新实现import { Inbox } from novu/react; import { Archive, Check } from lucide-react; Inbox /* ... */ renderDefaultActions{(notification) ( div classNameflex gap-2 button titleMark as readCheck classNamew-4 h-4 //button button titleArchiveArchive classNamew-4 h-4 //button /div )} /renderCustomActions只换主 / 次操作按钮的样式当你只想匹配品牌按钮风格、又不想重新实现默认操作时用这个 prop 最合适Inbox /* ... */ renderCustomActions{(notification) ( div classNameflex gap-2 mt-3 {notification.secondaryAction ( button classNamepx-3 py-1 rounded border border-gray-300 text-sm {notification.secondaryAction.label} /button )} {notification.primaryAction ( button classNamepx-3 py-1 rounded bg-pink-500 text-white text-sm {notification.primaryAction.label} /button )} /div )} /renderNotification整条通知完全接管这是核选项——你会失去所有内置交互能力请谨慎使用Inbox /* ... */ renderNotification{(notification) ( article classNamep-4 border-b header classNameflex justify-between h3 classNamefont-semibold{notification.subject}/h3 time classNametext-xs text-gray-500 {new Date(notification.createdAt).toLocaleString()} /time /header p{notification.body}/p /article )} /实现细节可参考 packages/js/src/ui/components/Notification/Notification.tsx当传入renderNotification时直接通过ExternalElementRenderer挂载自定义渲染否则回退到DefaultNotification并把renderAvatar、renderSubject、renderBody、renderDefaultActions、renderCustomActions逐项透传给默认行。注意 packages/js/src/ui/components/Inbox.tsx 中renderNotification与其余五个细粒度 render prop 是互斥的TypeScript 层面已做了联合类型约束传入renderNotification时不能再传其他 render prop。条件显示基于通知元数据分支渲染renderNotification会收到完整的通知对象包含tags、data、severity、workflow等字段字段结构见 packages/js/src/types.ts 中的 Notification 类型。你可以根据其中任意信号决定 UI 分支。按工作流标签tags分支renderNotification{(notification) { if (notification.tags?.includes(billing)) { return BillingRow notification{notification} /; } return DefaultRow notification{notification} /; }}按工作流标识符workflow identifier分支renderNotification{(notification) { if (notification.workflow?.identifier comment-mention) { return CommentMentionRow notification{notification} /; } return DefaultRow notification{notification} /; }}按 data 对象分支data是触发通知时随 payload 传入的自定义数据对象可在 In-App 步骤的 data 对象中定义 keyrenderNotification{(notification) { if (notification.data?.priority high) { return ( div classNamebg-red-50 ring-1 ring-red-300 p-3 rounded-lg strong{notification.subject}/strong p{notification.body}/p /div ); } return DefaultRow notification{notification} /; }}按严重程度severity分支严重程度来自 In-App 步骤的 severity 设置import { SeverityLevelEnum } from novu/react; renderNotification{(notification) { if (notification.severity SeverityLevelEnum.HIGH) { return UrgentRow notification{notification} /; } return DefaultRow notification{notification} /; }}在通知内容中使用 HTML默认情况下Novu 会对subject和body做净化sanitize以防范 XSS。要允许富 HTML需要两步在工作流中——打开 In-App 步骤编辑器打开Disable content sanitization禁用内容净化开关在 Inbox 中——用dangerouslySetInnerHTML渲染对应字段。只有在你完全掌控触发 payload 时才应开启此功能。原始 HTML 会打开 XSS 攻击面。仅正文Body渲染 HTMLInbox /* ... */ renderBody{(notification) ( div dangerouslySetInnerHTML{{ __html: notification.body }} / )} /仅主题Subject渲染 HTMLInbox /* ... */ renderSubject{(notification) ( span dangerouslySetInnerHTML{{ __html: notification.subject }} / )} /主题 正文同时渲染 HTMLInbox /* ... */ renderNotification{(notification) ( div classNamep-4 border-b h3 dangerouslySetInnerHTML{{ __html: notification.subject }} / div dangerouslySetInnerHTML{{ __html: notification.body }} / /div )} /工作流内容示例同时兼容 Liquid 变量与 HTML 标签{{subscriber.firstName}}, bgood news!/b Your ianalytics dashboard/i is ready. a hrefhttps://app.example.com/analytics target_blankOpen it/a.通知点击行为路由与回调routerPush接入 SPA 路由当通知在工作流中定义了redirect.url时Novu 会调用routerPush(url)让导航始终停留在你的 SPA 路由内部。各框架接入方式// Next.js App Router import { useRouter } from next/navigation; const router useRouter(); Inbox /* ... */ routerPush{(path) router.push(path)} /;// React Router v6 import { useNavigate } from react-router-dom; const navigate useNavigate(); Inbox /* ... */ routerPush{(path) navigate(path)} /;// Remix import { useNavigate } from remix-run/react; const navigate useNavigate(); Inbox /* ... */ routerPush{(path) navigate(path)} /;// Gatsby import { navigate } from gatsby; Inbox /* ... */ routerPush{(path) navigate(path)} /;routerPush的类型为(path: string) void见 packages/js/src/ui/types.ts在 packages/js/src/ui/components/Renderer.tsx 中通过InboxProvider注入整个组件树。onNotificationClick完全接管点击行为适合打开抽屉、模态框等自定义交互Inbox /* ... */ onNotificationClick{(notification) { if (notification.data?.entity issue) { openIssueDrawer(notification.data.entityId); return; } if (notification.redirect?.url) { window.location.href notification.redirect.url; } }} /onPrimaryActionClick/onSecondaryActionClick主次操作回调通知上的主 / 次操作按钮点击会分别触发对应回调可用于接收入邀请等业务动作Inbox /* ... */ onPrimaryActionClick{(notification) acceptInvite(notification.data.inviteId)} onSecondaryActionClick{(notification) declineInvite(notification.data.inviteId)} /自定义弹层Inbox 原语的可组合性Inbox组件是可组合的。当传入 children 时它充当一个 Context Provider——你可以把通知流放进任何弹层、抽屉或页面中。所有定制 propappearance、localization、tabs、routerPush、render props、context都会通过InboxProvider 自动流向下层子组件。组件渲染内容Bell /铃铛图标触发器Notifications /头部 可滚动列表 底部不含 Preferences 页面InboxContent /与Notifications /相同另加 Preferences 页面Preferences /独立的偏好设置从 packages/js/src/ui/components/Renderer.tsx 可以看到Notifications与Preferences本质上是对InboxContent的封装分别以InboxPage.Notifications/InboxPage.Preferences作为初始页并隐藏导航。组件由NovuProvider、LocalizationProvider、AppearanceProvider、InboxProvider逐层包裹因此配置天然向所有子组件生效。独立通知流无弹层import { Inbox, Notifications } from novu/react; Inbox applicationIdentifierYOUR_NOVU_APP_ID subscriberIdsubscriber-123 Notifications / /Inbox基于 Radix UI 的 Popoverimport { Inbox, InboxContent, Bell } from novu/react; import { Popover, PopoverTrigger, PopoverContent } from radix-ui/react-popover; Inbox applicationIdentifierYOUR_NOVU_APP_ID subscriberIdsubscriber-123 Popover PopoverTrigger Bell / /PopoverTrigger PopoverContent classNameh-[600px] w-[400px] p-0 InboxContent / /PopoverContent /Popover /Inbox基于 shadcn Drawer 的自定义弹层use client; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger, } from /components/ui/drawer; import { Inbox, InboxContent } from novu/react; export function NotificationDrawer() { return ( Inbox applicationIdentifierYOUR_NOVU_APP_ID subscriberIdsubscriber-123 Drawer directionright DrawerTrigger classNamerounded-full border px-4 py-2 Notifications /DrawerTrigger DrawerContent classNamew-[400px] DrawerHeader DrawerTitleInbox/DrawerTitle /DrawerHeader InboxContent / /DrawerContent /Drawer /Inbox ); }全页通知中心import { Inbox, InboxContent } from novu/react; export default function NotificationsPage() { return ( main classNamemax-w-3xl mx-auto py-12 h1 classNametext-2xl font-semibold mb-6Notifications/h1 Inbox applicationIdentifierYOUR_NOVU_APP_ID subscriberIdsubscriber-123 InboxContent / /Inbox /main ); }本地化覆盖界面文案localizationprop 允许你按 locale 覆盖 Inbox 的 UI 文案。注意本地化只影响 UI 外壳文案要翻译通知内容本身请使用工作流的 Workflow Translations 功能相关文档位于 docs/platform/workflow 目录。Inbox applicationIdentifierYOUR_NOVU_APP_ID subscriberIdsubscriber-123 localization{{ locale: en-US, inbox.filters.labels.default: Notifications, inbox.filters.labels.unread: Unread, inbox.filters.labels.archived: Archived, inbox.filters.labels.snoozed: Snoozed, inbox.filters.dropdownOptions.unread: Unread only, inbox.filters.dropdownOptions.default: All, notifications.emptyNotice: Youre all caught up., notifications.actions.readAll: Mark all as read, notifications.actions.archiveAll: Archive all, notifications.actions.archiveRead: Archive read, notification.actions.read.tooltip: Mark as read, notification.actions.unread.tooltip: Mark as unread, notification.actions.archive.tooltip: Archive, notification.actions.unarchive.tooltip: Unarchive, notification.actions.snooze.tooltip: Snooze, notification.actions.unsnooze.tooltip: Unsnooze, notification.snoozedUntil: Snoozed until, snooze.options.anHourFromNow: An hour from now, snooze.options.inOneDay: Tomorrow, snooze.options.inOneWeek: Next week, snooze.options.customTime: Custom time..., preferences.title: Notification Preferences, preferences.global: Global Preferences, preferences.emptyNotice: No notification specific preferences yet., dynamic: { new-comment-on-post: Post comments, new-follower-digest: New Follower Updates, }, }} /所有可用的默认 key 清单见 packages/js/src/ui/config/defaultLocalization.ts例如默认的inbox.filters.labels.default为Inbox、notifications.emptyNotice为Quiet for now. Check back later.未覆盖的 key 会沿用这些默认值。本地化工作流名称localization.dynamic是RecordworkflowId, string类型用于在 Preferences偏好设置UI 中显示友好的工作流名称localization{{ dynamic: { weekly-digest: Weekly Digest, team-mention: Team Mentions, }, }}多语言切换模式结合 React state 即可实现运行时语言切换const [locale, setLocale] useState(en-US); const localizationByLocale { en-US: { preferences.title: Notification Preferences, locale: en-US }, es-ES: { preferences.title: Preferencias de Notificación, locale: es-ES }, fr-FR: { preferences.title: Préférences de Notification, locale: fr-FR }, }; Inbox /* ... */ localization{localizationByLocale[locale]} /;Tabs分组过滤通知tabsprop 可以把通知按条件分组为多个过滤 Tabimport { Inbox, SeverityLevelEnum } from novu/react; Inbox applicationIdentifierYOUR_NOVU_APP_ID subscriberIdsubscriber-123 tabs{[ { label: All, filter: { tags: [] } }, { label: Promotions, filter: { tags: [promotions] } }, { label: Security, filter: { tags: [security, alert] } }, { label: Critical, filter: { severity: SeverityLevelEnum.HIGH } }, { label: High Priority, filter: { data: { priority: high } } }, { label: Billing High, filter: { tags: [billing], data: { priority: high } }, }, ]} /;过滤语义说明tags工作流级别的标签多个标签之间是OR逻辑severity来自 In-App 步骤的严重程度接受单个值或数组data匹配 In-App 步骤 data 对象中定义的 key可以组合tagsdataseverity实现更窄的过滤条件。从源码看Tab类型定义于 packages/js/src/ui/types.tsfilter取NotificationFilter中的tags、data、severity三项而 packages/js/src/types.ts 中的NotificationFilter还支持read、archived、snoozed、createdGte、createdLte等条件data过滤支持标量精确匹配、Scalar[]任一命中即匹配、{ or: [...] }、{ and: [{ or: [...] }, ...] }以及一层嵌套对象跨 key 之间为 AND 关系。如需展示每个 Tab 的未读数角标可使用useCountshook。实战配方品牌对齐、完全个性化的 Inbox把前面的能力组合起来即可得到一整套品牌化的 Inbox——深色主题、品牌主色、圆角、按严重程度高亮、自定义头像、Tab 分组与自定义文案use client; import { useRouter } from next/navigation; import { Inbox, SeverityLevelEnum } from novu/react; import { dark } from novu/react/themes; export function BrandedInbox({ subscriberId, subscriberHash }) { const router useRouter(); return ( Inbox applicationIdentifier{process.env.NEXT_PUBLIC_NOVU_APP_ID!} subscriberId{subscriberId} subscriberHash{subscriberHash} routerPush{(path) router.push(path)} tabs{[ { label: All, filter: { tags: [] } }, { label: Mentions, filter: { tags: [mention] } }, { label: Critical, filter: { severity: SeverityLevelEnum.HIGH } }, ]} appearance{{ baseTheme: dark, variables: { colorPrimary: #FB4CA3, colorPrimaryForeground: #FFFFFF, borderRadius: 12px, }, elements: { notification: ({ notification }) notification.data?.priority high ? bg-red-500/10 ring-1 ring-red-500/30 : , notificationPrimaryAction__button: bg-pink-500 hover:bg-pink-600, }, }} renderAvatar{(notification) ( img src{notification.avatar ?? /default-avatar.png} alt classNamew-8 h-8 rounded-full ring-1 ring-white/10 / )} localization{{ locale: en-US, inbox.filters.labels.default: All, notifications.emptyNotice: Nothing new — youre all caught up., }} / ); }配方中涉及的能力均可在源码中找到对应支撑appearance的variables与elements含回调式 elements类型定义在 packages/js/src/ui/types.tssubscriberHash用于对 subscriber 身份做 HMAC 鉴权routerPush与tabs经 packages/js/src/ui/components/Renderer.tsx 中的 Provider 注入到整个组件树。小结Novu Inbox 的个性化体系可以概括为四条主线Render Props负责逐块替换 保留默认能力条件渲染让通知行按 tags / workflow / data / severity 任意分支可组合原语Bell、Notifications、InboxContent、Preferences把通知流嵌入弹层、抽屉或整页localization / tabs / appearance则覆盖文案、分组与视觉主题。实际接入时建议优先使用细粒度 render prop保留默认操作仅在需要完全掌控交互时才使用renderNotification并在开启 HTML 渲染前确认 payload 完全可信。【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表