@novu/stateless 无状态通知框架实战:Provider 注册、模板编排与 trigger 触发全解析

发布时间:2026/9/10 11:14:58
@novu/stateless 无状态通知框架实战:Provider 注册、模板编排与 trigger 触发全解析 novu/stateless 无状态通知框架实战Provider 注册、模板编排与 trigger 触发全解析【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu本文基于 Novu 仓库中 packages/stateless/README.md 展开。novu/stateless是 Novu 通知基础设施中一个轻量、无状态stateless的通知管理框架包它不依赖数据库与后台服务只凭内存中的 Provider、Template 与 Theme 三大存储即可在任意 Node.js 应用中完成多通道邮件/SMS/聊天/推送消息的模板渲染与发送。读完本文你将掌握该包从安装、Provider 注册、模板注册到事件触发的完整链路并理解其底层引擎与源码实现。一、包概览它解决什么问题novu/stateless的定位是Notification Management Framework通知管理框架当前仓库中版本为2.6.6见 package.json采用 MIT 协议支持 CommonJS 与 ESM 双构建产物main指向dist/cjs/index.jsmodule指向dist/esm/index.js对 Node.js 的引擎要求为10。其核心设计思想可以用一句话概括用一套统一 API 抽象所有通知渠道。开发者不再需要针对每家邮件/SMS/IM 服务商分别接入 SDK而是面向 Novu 的统一 Provider 接口编程之后可以随时在底层切换或混用多家服务商而不改动业务代码。整个包非常轻量运行时依赖只有三个见 package.jsonhandlebars负责消息模板的编译与渲染lodash.get用于按路径安全地取出触发载荷中的变量lodash.merge用于合并默认配置与用户传入配置。从源码结构看packages/stateless/src/lib包被清晰拆分为六大内部模块模块源码路径职责NovuStateless主类novu.ts对外 API 门面持有各 Store 并对外暴露注册/触发方法Provider 存储provider/provider.store.ts按 id 或 channel 管理已注册的渠道 ProviderTemplate 存储template/template.store.ts保存模板并按触发载荷筛选激活消息Theme 存储theme/theme.store.ts管理邮件主题品牌化布局及默认主题内容引擎content/content.engine.ts基于 Handlebars 的模板编译与变量提取触发引擎trigger/trigger.engine.ts触发一次事件时串联上述全部组件完成发送无状态的含义在于模板、Provider 与主题全部存放在进程内的内存存储中通过register*系列方法写入无需任何数据库或外部服务。这使得它非常适合嵌入到已有 Node 服务中作为通知发送函数库使用同时它也是 Novu 完整平台中代码优先code-first场景的基石。二、安装与导入2.1 包管理器安装原 README 提供了 npm 与 yarn 两种安装方式npm install novu/statelessyarn add novu/stateless由于项目本身基于 pnpm workspace 管理见仓库根目录 pnpm-workspace.yaml在该仓库内也可以使用 pnpm 安装或直接引用本地包pnpm add novu/stateless2.2 按需导入包通过 src/index.ts 统一导出主要导出项包括import { NovuStateless, ChannelTypeEnum } from novu/stateless; import type { ITemplate, IMessage, ITriggerPayload, IEmailProvider, ISmsProvider, IChatProvider, IPushProvider, } from novu/stateless;其中NovuStateless是核心类ChannelTypeEnum是渠道类型枚举email/sms/chat/push/tool见 template.interface.ts。Provider 实现类则从novu/providers导入例如 README 示例中的SendgridEmailProvider。该包位于 packages/providers/src聚合了全部渠道服务商的实现。三、五分钟快速上手以下代码完整复刻 README 的 Usage 示例并补充了必要的类型与注释是一个可以直接复制运行的最小闭环import { NovuStateless, ChannelTypeEnum } from novu/stateless; import { SendgridEmailProvider } from novu/providers; // 1. 创建无状态实例 const novu new NovuStateless(); // 2. 注册邮件渠道 Provider await novu.registerProvider( new SendgridEmailProvider({ apiKey: process.env.SENDGRID_API_KEY, from: sendermail.com, }), ); // 3. 注册模板一条密码重置通知 const passwordResetTemplate await novu.registerTemplate({ id: password-reset, messages: [ { subject: Your password reset request, channel: ChannelTypeEnum.EMAIL, template: Hi {{firstName}}! To reset your password click a href{{resetLink}}here./a {{#if organization}} img src{{organization.logo}} / {{/if}} , }, ], }); // 4. 触发事件 await novu.trigger(password-reset, { $user_id: USER IDENTIFIER, $email: testemail.com, firstName: John, lastName: Doe, organization: { logo: https://evilcorp.com/logo.png, }, });执行流程梳理new NovuStateless()创建实例内部自动初始化三个内存存储TemplateStore / ProviderStore / ThemeStore与默认的 Handlebars 内容引擎registerProvider把 Sendgrid 提供商注册进 ProviderStoreregisterTemplate将id: password-reset的模板写入 TemplateStoretrigger(password-reset, data)触发事件TriggerEngine 按 id 找到模板 → 渲染 Handlebars 模板 → 将结果通过 EmailHandler 交给 Sendgrid 发送。模板中的{{firstName}}、{{resetLink}}等占位符会被触发载荷中的同名键替换{{#if organization}}是 Handlebars 的条件块当载荷中存在organization对象时才渲染其中的图片标签。README 示例在触发时传入的organization.logo正是为了让该条件块生效。四、核心 API 详解4.1new NovuStateless(config?)构造函数接收可选的 INovuConfig用于注入自定义实现interface INovuConfig { channels?: { email?: { from?: { name: string; email: string }; }; }; variableProtection?: boolean; // 是否开启变量缺失保护默认 true templateStore?: TemplateStore; // 自定义模板存储 providerStore?: ProviderStore; // 自定义 Provider 存储 themeStore?: ThemeStore; // 自定义主题存储 contentEngine?: IContentEngine; // 自定义内容引擎 }从 novu.ts 的构造逻辑可以看到默认配置中variableProtection被置为true且用户配置会通过lodash.merge与默认配置深度合并。这意味着变量缺失保护默认开启如果模板中引用了载荷中不存在的变量触发时会直接抛出Missing variables passed. ...错误见下文 5.2 节。4.2registerProvider(provider)与registerProvider(providerId, provider)方法有两种重载形式novu.ts// 形式一只传 Provider 实例providerId 取 provider.id await novu.registerProvider(new SendgridEmailProvider({ ... })); // 形式二显式指定注册 id await novu.registerProvider(my-sendgrid, new SendgridEmailProvider({ ... }));注册后的 Provider 存入 ProviderStore。底层 ProviderStore 支持四种查询方式getProviderById(providerId)按注册时使用的 key 精确获取getProviderByInternalId(providerId)按 Provider 自身暴露的id属性查找getProviderByChannel(channel)按渠道类型查找若同一渠道注册多个取第一个getProviders()列出全部已注册 Provider。4.3registerTemplate(template)参数是 ITemplateinterface ITemplate { id: string; // 模板唯一标识触发时用 eventId 对应它 themeId?: string; // 可选绑定的主题 id messages: IMessage[];// 一条模板可包含多条消息如同时发邮件短信 }注册后返回该模板在 Store 中的实例。模板内部可以声明多条不同渠道的消息从而一次触发同时向用户发送多通道通知例如邮件 短信await novu.registerTemplate({ id: welcome, messages: [ { channel: ChannelTypeEnum.EMAIL, subject: Welcome!, template: Hi {{name}} }, { channel: ChannelTypeEnum.SMS, template: Welcome {{name}} }, ], });4.4trigger(eventId, data)await novu.trigger(EVENT_NAME, { $user_id: USER IDENTIFIER, // 必填 $email: testemail.com, // 邮件渠道必填 firstName: John, // ... 任意自定义变量 });eventId必须与某个已注册模板的id一致否则会抛出Template on event: xxx was not found in the template store。第二个参数是 ITriggerPayload其中$前缀的键为框架保留字段字段说明$user_id必填用户唯一标识$email邮件渠道发送地址邮件消息发送时必填$phone已废弃deprecatedSMS 场景请使用$channelData$theme_id可选指定本次触发的主题$webhookUrl已废弃请使用$channelData$channelData渠道级数据如聊天渠道的 webhook 地址等$attachments附件列表可声明channels限定发送渠道$branding品牌信息在邮件模板 payload 中注入$branding除保留字段外其余键值会被原样注入 Handlebars 模板供渲染使用。五、底层原理TriggerEngine 的完整调用链trigger的实质是新建一个 TriggerEngine 并把模板、Provider、主题、内容引擎与配置一并传入。其内部按如下顺序执行5.1 模板查找与消息筛选const template await this.templateStore.getTemplateById(eventId); const activeMessages: IMessage[] await this.templateStore.getActiveMessages(template, data);getActiveMessages见 template.store.ts会对模板内每条消息执行激活判定active字段为undefined未设置时消息默认激活为布尔值时取其本身为函数时用触发载荷调用该函数异步求值。这给了开发者极强的灵活性——同一模板可以根据业务条件动态决定本次触发发不发某条消息messages: [ { channel: ChannelTypeEnum.EMAIL, template: ..., active: (payload) payload.user.hasEmail, // 按载荷动态开关 }, ],5.2 变量缺失保护variableProtectionTriggerEngine 在发送前会先做变量体检trigger.engine.ts用内容引擎的extractMessageVariables从模板字符串与 subject 中提取所有 Handlebars 变量用lodash.get逐一检查这些变量在载荷中是否存在若存在缺失且config.variableProtection为true抛出Missing variables passed. 缺失变量列表错误。注意这里使用的是_get(data, variable)即支持点路径模板中的{{organization.logo}}会被提取为organization.logo检查的正是载荷中data[organization][logo]是否存在。若不需要此保护可在构造实例时传入variableProtection: false。5.3 自定义校验器每条消息可以携带validatortemplate.interface.tsvalidator?: { validate(payload: ITriggerPayload): Promiseboolean | boolean; };TriggerEngine 在发送前会调用message.validator.validate(data)返回false时抛出Payload for ${channel} is invalid用于在发送前拦截非法载荷。5.4 事件钩子pre:send 与 post:sendNovuStateless继承自 Node 原生EventEmitterTriggerEngine 在真正发送前后各发出一个事件[trigger.engine.ts](https://link.gitcode.com/i/41885d96e76a4fa24948f1d22c944222#L53-L58, L81-L86)novu.on(pre:send, ({ id, channel, message, triggerPayload }) { // 发送前的钩子可在此埋点、审计、做限流 }); novu.on(post:send, ({ id, channel, message, triggerPayload }) { // 发送后的钩子可在此记录发送结果 });5.5 主题解析与渠道分发TriggerEngine 按载荷$theme_id 模板themeId 默认主题的优先级解析主题trigger.engine.ts随后根据 Provider 的channelType将消息分发给对应的 HandlerEMAIL→ EmailHandlerSMS→ SmsHandlerCHAT→ ChatHandler每个 Handler 负责把IMessage转换为该渠道 Provider 要求的IEmailOptions/ISmsOptions/IChatOptions并调用provider.sendMessage(...)。5.6 EmailHandler 细节模板、主题与附件以邮件为例EmailHandler.send 完成的工作包括从载荷中过滤出适用于邮件渠道的附件$attachments中channels未声明或包含email的项将$branding注入渲染上下文templatePayload { $branding, ...data }模板渲染template与textTemplate均支持字符串或异步函数两种形式subject支持字符串或同步函数否则抛错主题包装若配置了主题用getEmailLayout()取得外层 HTML 布局把渲染后的body作为变量嵌入同时合并getTemplateVariables()返回的主题变量发送前校验$email必填缺失时抛出$email on the trigger payload is missing...。六、主题Theme机制除了 README 中直接展示的 API主题是 stateless 框架中较容易被忽略但很实用的能力。通过 ThemeStore 与 theme.interface.ts可以为邮件提供品牌化的统一 HTML 外壳const theme { branding: { mainColor: #ff3519, logo: https://example.com/logo.png }, emailTemplate: { getEmailLayout() { return div style...h1{{branding.mainColor}}/h1{{{body}}}/div; }, getTemplateVariables() { return { branding: { mainColor: #ff3519 } }; }, }, }; await novu.registerTheme(branded, theme); await novu.setDefaultTheme(branded);之后每次触发邮件都会自动套用该主题布局也可在单次触发时通过载荷$theme_id覆盖或在模板上声明themeId绑定指定主题。主题解析的优先级链为载荷$theme_id 模板themeId 默认主题见 trigger.engine.ts。七、Provider 生态与渠道支持原 README 用清单形式列出了各渠道的 Provider 支持情况。在仓库当前状态下packages/providers/src 聚合了这些实现各渠道代表性 Provider 如下邮件Email已支持Sendgrid、Netcore、Mailgun、SES、Postmark、自定义 SMTPNodemailer、Mailjet、Mandrill、SendinBlue 待支持SparkPost。短信SMS已支持Twilio、Plivo、SNS、NexmoVonage、Sms77、Telnyx、Termii、Gupshup 待支持Bandwidth、RingCentral。推送Push已支持FCM、Expo 待支持SNS、Pushwoosh。聊天Chat已支持Slack、Discord 待支持MS Teams、Mattermost。应用内In-App已支持Novu 自研通知中心即本仓库 packages/novu 所提供的 Inbox / 通知中心能力。其他规划中PagerDuty。说明以上清单忠实反映 README 写作时的状态已支持/待支持以勾选标记为准其中标为未勾选的项不代表仓库中完全没有代码仅代表该清单当时未将其列为完成状态请以实际源码为准。从 Provider 接口provider.interface.ts可以看到所有 Provider 都遵循统一的契约必须实现id、channelType与sendMessage(options, bridgeProviderData)。发送成功统一返回ISendMessageSuccessResponse可包含id/ids/date/channel回调解析则通过可选的getMessageId与parseEventBody实现。这套统一接口正是一套 API 管所有渠道的根基——切换服务商时只需替换 registerProvider 传入的实例业务代码零改动。八、测试验证与可观测入口仓库为该包提供了完整的单元测试可作为理解各组件行为的活文档novu.spec.ts主类 API 行为测试trigger.engine.spec.ts触发引擎全链路测试模板查找、Provider 解析、变量保护等content.engine.spec.tsHandlebars 渲染与变量提取测试template.store.spec.ts 与 provider.store.spec.ts两个内存存储的行为测试email.handler.spec.ts、sms.handler.spec.ts、chat.handler.spec.ts各渠道 Handler 的发送逻辑测试。若要在本仓库内运行这些测试可在packages/stateless目录下执行package.json 中定义了test:unit脚本pnpm --filter novu/stateless test:unit九、与其他 Novu 包的关系与适用场景novu/stateless是 Novu 生态中的轻量内核与平台其他组件互补novu/providerspackages/providersProvider 实现库stateless 的 Provider 实例通常从这里取novu/frameworkpackages/framework面向代码优先工作流的完整框架提供 step、control 等更高层抽象novu/js/novu/reactpackages/js、packages/reactInbox 前端 SDK对应 README 中 In-App 渠道的能力apps/api/apps/worker等apps/api、apps/workerNovu 托管/自托管平台的完整后端stateless 的逻辑在平台内作为执行内核被复用。因此novu/stateless最适合以下场景已有后端服务、不想引入数据库只想在代码里声明式地注册模板与 Provider直接调用发送多通道统一抽象邮件、短信、聊天、推送一接口切换降低多服务商集成成本高度定制通过INovuConfig注入自定义templateStore、providerStore、themeStore或contentEngine把存储与渲染逻辑替换为任意实现事件驱动的通知流借助pre:send/post:send钩子与模板级active函数构建可观测、可动态开关的通知链路。简而言之novu/stateless用约千行源码实现了一个注册 Provider → 注册模板 → 触发事件 → 自动渲染并分发的完整通知内核。无论你是在评估 Novu 的架构设计还是准备在业务中快速落地多通道通知它都是一个低依赖、易上手、可深度定制的起点。【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考