@zero2x/sdk
v0.2.0
Published
Generic typed parent–iframe messaging SDK with origin validation
Readme
@zero2x/sdk
通用的父页面与内嵌 iframe 双向通信 SDK。调用方定义消息名与 payload 类型,SDK 负责握手、 来源校验、消息分发和资源释放,不规定业务通信类型。
npm 包 · 当前版本:0.2.0
无需框架或运行时依赖,可用于原生 JavaScript、React、Vue 等浏览器项目。包提供 ESM、CommonJS 和 TypeScript 类型声明。
安装
父页面和 iframe 页面都需要安装:
npm install @zero2x/sdk
# 或
pnpm add @zero2x/sdk定义消息
在接入项目中共享类型定义;消息名可以是任意非空字符串:
export interface HostMessages {
configure: { locale: string; theme: 'light' | 'dark' };
}
export interface ChildMessages {
resize: { height: number };
completed: { id: string };
}第一个泛型定义发送的消息,第二个泛型定义接收的消息。只传一个泛型时,双向使用同一映射。
传入泛型后,send 与 on 根据消息名推断 payload,并检查消息名及参数类型。
父页面接入
import { createParentClient } from '@zero2x/sdk/parent';
import type { HostMessages, ChildMessages } from './message-types';
const iframe = document.querySelector<HTMLIFrameElement>('#embedded-frame');
if (!iframe) throw new Error('iframe not found');
const client = createParentClient<HostMessages, ChildMessages>({
iframe,
targetOrigin: 'https://embedded.example.com',
});
const unsubscribe = client.on('resize', ({ height }) => {
iframe.style.height = `${height}px`;
});
await client.connect();
client.send('configure', { locale: 'zh-CN', theme: 'light' });
// 不再使用时调用;不要在刚建立连接后立即销毁。
// unsubscribe();
// client.destroy();iframe 页面接入
import { createIframeClient } from '@zero2x/sdk/iframe';
import type { HostMessages, ChildMessages } from './message-types';
// 子页面的发送与接收方向和父页面相反。
const client = createIframeClient<ChildMessages, HostMessages>({
parentOrigin: 'https://host.example.com',
});
const unsubscribe = client.on('configure', ({ locale, theme }) => {
document.documentElement.lang = locale;
document.documentElement.dataset.theme = theme;
});
await client.ready();
client.send('resize', { height: document.documentElement.scrollHeight });
// 不再使用时调用:unsubscribe(); client.destroy();不指定泛型
不指定泛型时,可以发送任意非空消息名与 payload;接收的 payload 为 unknown,由调用方校验后使用:
const client = createParentClient({ iframe, targetOrigin });
client.on('custom.event', (payload) => {
// payload: unknown,在这里执行调用方的业务校验。
});
await client.connect();
client.send('custom.event', { value: 1 });泛型只提供编译期检查,不验证远端数据。payload 可以是对象、数组、字符串、数字、null 或
undefined 等浏览器 structured clone 支持的数据;函数等不可克隆内容会抛出
SDKError(SERIALIZATION_FAILED)。SDK 不提供 RPC、消息持久化或送达确认;发送成功仅表示
postMessage 调用成功。
从 0.1.0 升级
- 不再自动合并内置
USER_INFO消息,也不根据消息名强制校验token + lang。 SDK_MESSAGE_TYPE、SDKUserInfo、BuiltinMessages和isSDKUserInfo保留为可选的兼容导出。- 旧调用没有泛型时仍可发送用户信息。需要旧类型约束时显式使用
BuiltinMessages:
import { type BuiltinMessages, isSDKUserInfo, SDK_MESSAGE_TYPE } from '@zero2x/sdk';
const client = createParentClient<BuiltinMessages>({ iframe, targetOrigin });
await client.connect();
client.send(SDK_MESSAGE_TYPE.USER_INFO, { token: '', lang: 'zh-CN' });
// 接收端需要运行时校验时,在自己的回调里调用 isSDKUserInfo(payload)。原先使用自定义映射且依赖内置消息的调用,需要改为
createParentClient<BuiltinMessages & CustomMessages>(options);iframe 同理。
通信信封与握手版本保持不变;两端沿用原 USER_INFO 数据时可以与旧客户端通信,但旧客户端
仍会拒绝该消息名下不符合旧结构的数据。
API
| 工厂 | 说明 |
| ------------------------------------------------- | ------------- |
| createParentClient<Outgoing, Incoming>(options) | 父页面客户端 |
| createIframeClient<Outgoing, Incoming>(options) | iframe 客户端 |
| 选项 | 适用端 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ------------------- | ---- | ---------------------------- |
| iframe | 父页面 | HTMLIFrameElement | 是 | 要通信的 iframe 元素 |
| targetOrigin | 父页面 | string | 是 | iframe 的精确 origin |
| parentOrigin | iframe | string | 是 | 父页面的精确 origin |
| connectTimeout | 两端 | number | 否 | 握手超时毫秒数,默认 10000 |
| 方法 | 说明 |
| --------------------- | ---------------------------------------------- |
| connect() | 父页面等待 iframe 完成握手 |
| ready() | iframe 注册监听器并与父页面完成握手 |
| send(type, payload) | 向对端发送消息;必须先等待握手完成 |
| on(type, listener) | 订阅对端消息,返回取消订阅函数;可在握手前调用 |
| destroy() | 释放监听器、定时器和消息订阅者 |
错误处理
握手超时或连接被销毁时,connect() / ready() 的 Promise 会被拒绝;未连接就发送、
消息名为空或 payload 无法克隆时,send() 会同步抛出 SDKError。调用方负责处理这些错误:
import { SDKError } from '@zero2x/sdk';
try {
await client.connect();
client.send('configure', { locale: 'zh-CN', theme: 'light' });
} catch (error) {
if (error instanceof SDKError) {
console.error('iframe 通信失败:', error.code, error.message);
} else {
throw error;
}
}常见错误码:INVALID_CONFIG、CONNECT_TIMEOUT、NOT_CONNECTED、INVALID_MESSAGE、
SERIALIZATION_FAILED 和 DESTROYED。订阅回调中的业务异常由调用方自行处理。
来源与生命周期
- 两端必须配置精确的 HTTP/HTTPS origin,例如
https://embedded.example.com;不允许*或带路径的 URL。 - 不要从不可信查询参数直接读取 origin;SDK 同时校验消息的 origin、来源窗口及当前连接 ID。
- 页面卸载或 iframe 永久移除时调用
destroy(),不再订阅时调用unsubscribe()。 - 泛型与来源校验不能替代业务数据校验、身份认证和权限判断。
SDK 不在导入时访问 window 或注册监听器,可以被 SSR 工具链解析;实例只能在浏览器中创建。
子页面客户端面向内嵌 iframe,当前不覆盖 popup/opener 场景。
手工验收
- 父页面和 iframe 分别按上述示例配置精确 origin,交换双向泛型;握手后发送
configure、resize,预期两端各收到对应 payload。 - 在同一映射中分别用对象、数组和
null作为 payload,预期原样接收;业务校验由调用方处理。 - 从其他窗口或其他 origin 发送相同信封,预期订阅回调不被触发。
- 取消订阅或销毁客户端后再次发送,预期回调不再执行;重建 iframe 客户端并重新握手后可继续通信。
维护者发布
在 monorepo 根目录执行:
# 先更新 package.json 版本,以及本 README 和 CHANGELOG。
npm login --registry=https://registry.npmjs.org/
pnpm publish:sdk
# 只执行包级验证、构建并预览发布内容,不上传 npm。
pnpm publish:sdk --dry-run脚本复用 SDK 的 prepublishOnly 完成检查、测试与构建,发布到公开 npm registry 的 latest
标签。身份检查或任一步骤失败时立即停止;不会自动修改版本、提交或推送 Git。
