@honeeasy/notification-jssdk
v2.0.0-beta.1
Published
UI-free Honeeasy Notification browser SDK
Readme
Honeeasy Notification JSSDK 2.x
Honeeasy Notification JSSDK 2.x 是无 UI 的浏览器通知协议客户端。SDK 不创建 DOM、不注入 CSS 或字体,
不包含 iframe、Widget 或 Honeeasy 静态资源地址。宿主应用收到 NotificationHandle 后自行决定 toast、
全屏、动画、样式和交互。
完整第三方接入说明见 docs/third-party-integration.md。面向编码助手的集成 Skill 见 skills/integrate-honeeasy-notifications/SKILL.md。
安装与产物
npm install @honeeasy/[email protected]发布包包含:
dist/index.mjs:npm ESM;dist/index.cjs:CommonJS;dist/honeeasy-notification-jssdk.umd.js:可部署到接入方自己服务器的 UMD;dist/index.d.ts:TypeScript 声明。
SignalR 已打入三种 JavaScript 产物。接入项目不需要安装 Vue、React、jQuery 或 SignalR。已构建产物 使用 ES2017 语法,可被 Node 12/14/16 时代的构建器消费;构建本 SDK 源码需要 Node 20.19+。
基本用法
import { createHoneeasyNotification } from "@honeeasy/notification-jssdk";
const client = createHoneeasyNotification({
apiBaseUrl: "https://api.example.com",
ticketProvider: async () => {
const response = await fetch("/api/my-app/notification-ticket", { credentials: "include" });
const result = await response.json();
return result.ticket;
},
onMessage: handle => {
renderNotification(handle);
},
onError: error => reportToApplicationLogger(error),
});
await client.start();
// SPA 页面销毁时:await client.stop();浏览器只能取得 60 秒一次性 Ticket,不能持有 ClientSecret、应用访问令牌或内部 Register.Id。
ticketProvider 在启动、会话续期或 401 重建时会再次调用,每次必须返回新 Ticket。
NotificationHandle
interface NotificationHandle {
readonly message: Readonly<NotificationMessage>;
readonly expired: boolean;
markReceived(): Promise<NotificationOperationResult>;
markDisplayed(): Promise<NotificationOperationResult>;
markClicked(): Promise<NotificationOperationResult>;
markDismissed(): Promise<NotificationOperationResult>;
createTrackedRedirect(): Promise<string>;
track(name: string, properties?: Record<string, string | number | boolean | null>): Promise<NotificationOperationResult>;
}RECEIVED 默认由 SDK 自动上报;设置 autoAckReceived: false 后由宿主调用 markReceived()。
DISPLAYED 应在真实 UI 完成渲染后调用,CLICKED 和 DISMISSED 应在真实交互后调用。它们是客户端声明,
服务端只验证身份、消息范围、幂等和状态规则,不能验证实际 DOM 或物理点击。
外链必须先调用 createTrackedRedirect()。该方法在后端落库 CLICKED 后返回短期跳转地址,但 SDK 不执行导航:
button.addEventListener("click", async () => {
const redirectUrl = await handle.createTrackedRedirect();
window.location.assign(redirectUrl);
});自定义事件名必须匹配 ^[a-z][a-z0-9_.-]{0,63}$,不能使用 honeeasy.、sdk. 或标准 ACK 名;
最多 16 个属性,属性值只能是字符串、数字、布尔或 null。自定义事件不会修改标准消息状态机。
Vue 2/3
// 组件 mounted/onMounted 后启动;以下 showNotification 由应用自己实现。
const client = createHoneeasyNotification({
apiBaseUrl: window.NOTIFICATION_API_BASE_URL,
ticketProvider: () => fetch("/notification-ticket").then(r => r.json()),
onMessage(handle) {
notifications.value.push(handle);
nextTick(() => requestAnimationFrame(() => void handle.markDisplayed()));
},
onResolved({ messageId }) {
notifications.value = notifications.value.filter(item => item.message.messageId !== messageId);
},
});
onMounted(() => void client.start());
onBeforeUnmount(() => void client.stop());React
function NotificationProvider() {
const [notifications, setNotifications] = React.useState<NotificationHandle[]>([]);
React.useEffect(() => {
const client = createHoneeasyNotification({
apiBaseUrl: process.env.REACT_APP_NOTIFICATION_API!,
ticketProvider: () => fetch("/notification-ticket").then(r => r.json()),
onMessage: handle => setNotifications(current => [...current, handle]),
onResolved: ({ messageId }) => setNotifications(current =>
current.filter(item => item.message.messageId !== messageId)),
});
void client.start();
return () => { void client.stop(); };
}, []);
return <>{notifications.map(handle =>
<NotificationView key={handle.message.messageId} handle={handle} />)}</>;
}
function NotificationView({ handle }: { handle: NotificationHandle }) {
React.useLayoutEffect(() => { void handle.markDisplayed(); }, [handle]);
return <button onClick={() => void handle.markClicked()}>{handle.message.title}</button>;
}jQuery
var client = HoneeasyNotification.createHoneeasyNotification({
apiBaseUrl: "https://api.example.com",
ticketProvider: function () {
return fetch("/notification-ticket").then(function (r) { return r.json(); });
},
onMessage: function (handle) {
var $notice = $("<section>").addClass("my-notice").text(handle.message.title);
$notice.on("click", function () { handle.markClicked(); });
$("#notification-root").append($notice);
window.requestAnimationFrame(function () { handle.markDisplayed(); });
}
});
client.start();UMD 文件可以从接入方自己的域名加载:
<script src="/assets/vendor/honeeasy-notification-jssdk.umd.js"></script>也可以直接使用公共 CDN(生产环境建议固定具体版本):
<script src="https://cdn.jsdelivr.net/npm/@honeeasy/[email protected]/dist/honeeasy-notification-jssdk.umd.js"></script>UMD 全局变量仍然是 HoneeasyNotification;npm scope 不会改变浏览器全局名称。
原生 JavaScript
var client = HoneeasyNotification.createHoneeasyNotification({
apiBaseUrl: "https://api.example.com",
autoAckReceived: false,
ticketProvider: function () {
return fetch("/notification-ticket").then(function (r) { return r.json(); });
}
});
client.on("message", function (handle) {
handle.markReceived();
var notice = document.createElement("button");
notice.textContent = handle.message.title;
notice.onclick = function () { handle.markClicked(); };
document.querySelector("#notification-root").appendChild(notice);
requestAnimationFrame(function () { handle.markDisplayed(); });
});
client.start();以上 DOM 都由示例宿主创建,不属于 SDK 行为。
第三方服务端
- 服务端用
clientId + clientSecret调用POST /api/notification/v1/oauth/token。 - 服务端用 Bearer Token 调用
POST /api/notification/v1/tickets:
{
"activityId": 10001,
"subject": { "scheme": "credential-code", "value": "证件码" },
"origin": "https://receipt.partner.com"
}- Honeeasy 后端按
activityId + credential-code解析唯一 Register,并校验应用活动与 Origin 白名单。 - 第三方服务端只把一次性 Ticket 返回浏览器。
生产 Origin 必须是精确 HTTPS authority,不接受通配符、路径、查询、fragment 或 user-info。
运行时行为
SDK 负责 Session、SignalR 自动重连、提前五分钟续期、Pending 恢复、消息去重、页面可见性、
多标签页单可见宿主投递、过期清理,以及 IndexedDB(不可用时 localStorage/内存)ACK 重试。
重试沿用同一个操作 ID;网络错误、超时、429 和 5xx 使用指数退避。SDK 不清洗或渲染富文本,宿主在
v-html、dangerouslySetInnerHTML 或 innerHTML 前必须使用 DOMPurify 等白名单清洗器二次清洗。
旧环境按实际浏览器补充 Promise、URL、fetch、Headers、WebSocket、crypto.getRandomValues、
IndexedDB polyfill。SDK 不覆盖宿主全局对象。
