news 2026/9/10 11:14:46

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
@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.json):

  • handlebars:负责消息模板的编译与渲染;
  • lodash.get:用于按路径安全地取出触发载荷中的变量;
  • lodash.merge:用于合并默认配置与用户传入配置。

从源码结构看(packages/stateless/src/lib),包被清晰拆分为六大内部模块:

模块源码路径职责
NovuStateless主类novu.ts对外 API 门面,持有各 Store 并对外暴露注册/触发方法
Provider 存储provider/provider.store.ts按 id 或 channel 管理已注册的渠道 Provider
Template 存储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/stateless
yarn add @novu/stateless

由于项目本身基于 pnpm workspace 管理(见仓库根目录 pnpm-workspace.yaml),在该仓库内也可以使用 pnpm 安装或直接引用本地包:

pnpm add @novu/stateless

2.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: 'sender@mail.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: 'test@email.com', firstName: 'John', lastName: 'Doe', organization: { logo: 'https://evilcorp.com/logo.png', }, });

执行流程梳理:

  1. new NovuStateless()创建实例,内部自动初始化三个内存存储(TemplateStore / ProviderStore / ThemeStore)与默认的 Handlebars 内容引擎;
  2. registerProvider把 Sendgrid 提供商注册进 ProviderStore;
  3. registerTemplateid: 'password-reset'的模板写入 TemplateStore;
  4. trigger('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)

参数是 ITemplate:

interface 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: 'test@email.com', // 邮件渠道必填 firstName: 'John', // ... 任意自定义变量 });

eventId必须与某个已注册模板的id一致,否则会抛出Template on event: xxx was not found in the template store。第二个参数是 ITriggerPayload,其中$前缀的键为框架保留字段:

字段说明
$user_id必填,用户唯一标识
$email邮件渠道发送地址,邮件消息发送时必填
$phone已废弃(deprecated),SMS 场景请使用$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 变量缺失保护(variableProtection)

TriggerEngine 在发送前会先做变量体检(trigger.engine.ts):

  1. 用内容引擎的extractMessageVariables从模板字符串与 subject 中提取所有 Handlebars 变量;
  2. lodash.get逐一检查这些变量在载荷中是否存在;
  3. 若存在缺失且config.variableProtectiontrue,抛出Missing variables passed. <缺失变量列表>错误。

注意这里使用的是_get(data, variable),即支持点路径:模板中的{{organization.logo}}会被提取为organization.logo,检查的正是载荷中data['organization']['logo']是否存在。若不需要此保护,可在构造实例时传入variableProtection: false

5.3 自定义校验器

每条消息可以携带validator(template.interface.ts):

validator?: { validate(payload: ITriggerPayload): Promise<boolean> | boolean; };

TriggerEngine 在发送前会调用message.validator.validate(data),返回false时抛出Payload for ${channel} is invalid,用于在发送前拦截非法载荷。

5.4 事件钩子:pre:send 与 post:send

NovuStateless继承自 Node 原生EventEmitter,TriggerEngine 在真正发送前后各发出一个事件([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将消息分发给对应的 Handler:

  • EMAIL→ EmailHandler
  • SMS→ SmsHandler
  • CHAT→ ChatHandler

每个 Handler 负责把IMessage转换为该渠道 Provider 要求的IEmailOptions/ISmsOptions/IChatOptions并调用provider.sendMessage(...)

5.6 EmailHandler 细节:模板、主题与附件

以邮件为例,EmailHandler.send 完成的工作包括:

  • 从载荷中过滤出适用于邮件渠道的附件($attachmentschannels未声明或包含email的项);
  • $branding注入渲染上下文(templatePayload = { $branding, ...data });
  • 模板渲染:templatetextTemplate均支持字符串或异步函数两种形式,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、自定义 SMTP(Nodemailer)、Mailjet、Mandrill、SendinBlue; 待支持:SparkPost。

短信(SMS)已支持:Twilio、Plivo、SNS、Nexmo(Vonage)、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 都遵循统一的契约:必须实现idchannelTypesendMessage(options, bridgeProviderData)。发送成功统一返回ISendMessageSuccessResponse(可包含id/ids/date/channel),回调解析则通过可选的getMessageIdparseEventBody实现。这套统一接口正是"一套 API 管所有渠道"的根基——切换服务商时,只需替换 registerProvider 传入的实例,业务代码零改动

八、测试验证与可观测入口

仓库为该包提供了完整的单元测试,可作为理解各组件行为的活文档:

  • novu.spec.ts:主类 API 行为测试;
  • trigger.engine.spec.ts:触发引擎全链路测试(模板查找、Provider 解析、变量保护等);
  • content.engine.spec.ts:Handlebars 渲染与变量提取测试;
  • 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/providers(packages/providers):Provider 实现库,stateless 的 Provider 实例通常从这里取;
  • @novu/framework(packages/framework):面向代码优先工作流的完整框架,提供 step、control 等更高层抽象;
  • @novu/js/@novu/react(packages/js、packages/react):Inbox 前端 SDK,对应 README 中 In-App 渠道的能力;
  • apps/api/apps/worker等(apps/api、apps/worker):Novu 托管/自托管平台的完整后端,stateless 的逻辑在平台内作为执行内核被复用。

因此,@novu/stateless最适合以下场景:

  1. 已有后端服务、不想引入数据库:只想在代码里声明式地注册模板与 Provider,直接调用发送;
  2. 多通道统一抽象:邮件、短信、聊天、推送一接口切换,降低多服务商集成成本;
  3. 高度定制:通过INovuConfig注入自定义templateStoreproviderStorethemeStorecontentEngine,把存储与渲染逻辑替换为任意实现;
  4. 事件驱动的通知流:借助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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 11:11:56

毕业论文AI写作软件平台排行榜:选择要点与实用选择要点

摘要速览当前学术写作需求持续增长&#xff0c;AI写作工具成为学生、科研人员提升效率的重要辅助。本文围绕毕业论文AI写作软件的选型需求&#xff0c;梳理统一判断标准&#xff0c;盘点公开可核验的工具信息&#xff0c;明确适用边界与决策注意事项。e稿AI智能写作平台作为垂直…

作者头像 李华
网站建设 2026/9/10 11:06:14

无锡乡镇街道shp数据包:shapefile解析、坐标转换与GIS数据处理指南

简介&#xff1a;这份无锡市乡镇街道级矢量地图资源&#xff0c;面向GIS开发、城市规划与空间分析人员&#xff0c;提供无锡各区县与乡镇街道的行政区划边界数据&#xff0c;可直接用于地图绘制、区划统计、可视化展示或空间分析。压缩包共含21个文件&#xff0c;涵盖shp几何数…

作者头像 李华