简介
一个轻量级、类型友好的浏览器端消息通信库。适合同域多窗口通信、iframe 父子页面通信等。
提供了轻量易用的事件 API(on、once、emit、off等),几行代码即可实现在不同网页上下文之间通信。
默认基于 BroadcastChannel 实现同域通信,也提供了一个适用于跨域通信,基于 postMessage 的适配器。支持通过自定义适配器扩展通信方式。
核心代码不到 200 行(查看源码),单元测试覆盖率 100%。
特性
- 轻量易用:
on、once、emit、off即可完成事件收发。 - TypeScript 友好:通过
泛型约束事件名和事件数据类型。 - 可扩展适配器:默认
BroadcastChannel,可自定义适配器。 - 可自定义序列化:支持替换
JSON.stringify/parse。 - 错误可观察:提供消息编解码错误与底层消息错误回调。
- 独立事件系统:抽离了
Event bus实现,可以只引入该模块,用于组件通信等。
安装
pnpm add webpage-channel
# 或
npm i webpage-channel
# 或
yarn add webpage-channel
同域通信
该库默认基于 BroadcastChannel,同域页面通信时,可以直接使用默认配置。
1. 定义事件类型
在同域的两个不同页面,都添加以下代码:
import { WebpageChannel } from 'webpage-channel';
type Events = {
'user:update': (payload: { id: string; name: string }) => void;
'toast:show': (payload: { message: string; type: 'success' | 'error' }) => void;
};
const channel = new WebpageChannel<Events>('app-channel');
2. 监听消息
在接收消息方,添加以下代码:
channel.on('user:update', (payload) => {
console.log('收到用户更新', payload.id, payload.name);
});
3. 发送消息
在发送消息方,添加以下代码:
const ok = channel.emit('user:update', { id: 'u1', name: 'Alice' });
if (!ok) {
console.warn('消息发送失败');
}
4. 取消监听和销毁(可选)
const onToast = (payload: { message: string; type: 'success' | 'error' }) => {
console.log(payload.message);
};
channel.on('toast:show', onToast);
channel.once('toast:show', (payload) => {
console.log('仅触发一次:', payload.message);
});
channel.off('toast:show', onToast); // 移除指定监听器
channel.off('toast:show'); // 移除该事件全部监听器
channel.close(); // 清空监听并关闭底层通道
跨域通信
跨域通信可以使用内置适配器 PostMessageAdapter ,适合父页面与 iframe、弹窗窗口等基于 window.postMessage 的场景。
PostMessageAdapter 构造参数:
targetWindow: Window:目标窗口对象(如iframe.contentWindow、window.parent)。targetOrigin: string:目标来源(例如https://example.com,或开发环境*)。
1. 父页面发送消息给 iframe
import { PostMessageAdapter, WebpageChannel } from 'webpage-channel';
type Events = {
'auth:token': (payload: { token: string }) => void;
};
const iframe = document.getElementById('child-frame') as HTMLIFrameElement;
const adapter = new PostMessageAdapter(iframe.contentWindow!, 'https://child.example.com');
const channel = new WebpageChannel<Events>('iframe-channel', undefined, adapter);
channel.emit('auth:token', { token: 'abc123' });
2. iframe 接收消息
import { PostMessageAdapter, WebpageChannel } from 'webpage-channel';
type Events = {
'auth:token': (payload: { token: string }) => void;
};
const adapter = new PostMessageAdapter(window.parent, 'https://parent.example.com');
const channel = new WebpageChannel<Events>('iframe-channel', undefined, adapter);
channel.on('auth:token', (payload) => {
console.log('收到 token:', payload.token);
});
3. 注意事项
- 生产环境请避免使用
*作为targetOrigin。 PostMessageAdapter内部会同时校验e.origin === targetOrigin和e.source === targetWindow。WebpageChannel的第一个参数name,父子页面必须一致,内部会校验,不一致则接收不到消息。
跨组件通信
该库抽离了事件系统 Event Bus 的实现,作为一个单独模块提供,因此可以只引入该模块,用于组件通信。
1. 父组件
import { EventBus } from 'webpage-channel';
type Events = {
ping: (payload: { value: number }) => void;
pong: (payload: { result: string }) => void;
};
export const bus = new EventBus<Events>();
2. 兄弟组件 A
发送消息
import { bus } from 'parent'
bus.emit('ping', { value: 1 });
bus.emit('pong', { result: 'hello' })
3. 兄弟组件 B
接收消息
import { bus } from 'parent'
bus.on('ping', (payload) => {
console.log('收到:', payload.value);
// 触发后,主动移除该事件全部监听器
bus.off('ping')
});
// 只触发一次
bus.once('pong', (payload) => {
console.log('收到:', payload.result);
})
API
new WebpageChannel<T>(channelName, options?, adapter?)
创建一个频道实例。
channelName: string:频道名称。options?: { ... }:可选配置。adapter?: IWebpageChannelAdapter:可选适配器;不传时默认使用BroadcastChannelAdapter。
options 说明:
onError?: (e: Error) => void- 序列化、反序列化或事件分发过程中出现异常时触发。
onMessageError?: (e: MessageEvent) => void- 底层通道触发
messageerror时触发。
- 底层通道触发
serializeMessage?: (data) => string- 自定义序列化函数,默认
JSON.stringify。
- 自定义序列化函数,默认
deserializeMessage?: (raw) => data- 自定义反序列化函数,默认
JSON.parse。
- 自定义反序列化函数,默认
channel.on(event, callback)
注册事件监听。
channel.once(event, callback)
注册一次性监听器,首次触发后会自动移除。
channel.emit(event, payload): boolean
发送事件并返回是否发送成功:
true:序列化与发送成功。false:发送过程抛错(同时触发onError)。false:在调用close()之后再调用emit也会返回false(同时触发onError)。
channel.off(event, listener?)
- 传
listener:仅移除该函数引用。 - 不传
listener:移除该事件全部监听器。
channel.clear()
清空当前实例的所有事件监听器。
channel.close()
清空监听器并关闭底层适配器。
调用后,就不能通信了。必须重新创建新实例,才可以通信。
适配器扩展
库通过 IWebpageChannelAdapter 抽象底层通信能力,你可以按需实现自己的适配器(例如 window.postMessage、MessagePort 等)。
内置适配器
BroadcastChannelAdapter:默认适配器,适合同源多标签页/上下文通信。PostMessageAdapter:适合父页面与 iframe、弹窗窗口等基于window.postMessage的场景。
自定义适配器示例
import { WebpageChannel, type IWebpageChannelAdapter } from 'webpage-channel';
class MyAdapter implements IWebpageChannelAdapter {
postMessage(message: string) {
// send
}
onMessage(callback: (message: string) => void) {
// receive
}
onMessageError(callback: (e: MessageEvent) => void) {
// message error
}
close() {
// cleanup
}
}
type Events = {
ping: (payload: { time: number }) => void;
};
const channel = new WebpageChannel<Events>('my-channel', undefined, new MyAdapter());
序列化定制示例
type Events = {
notify: (payload: { text: string }) => void;
};
const channel = new WebpageChannel<Events>('secure-channel', {
serializeMessage(data) {
return btoa(JSON.stringify(data));
},
deserializeMessage(raw) {
return JSON.parse(atob(raw));
},
onError(err) {
console.error('编解码或分发错误:', err);
}
});
使用建议
- 事件名保持稳定且语义化,推荐使用
模块:动作命名。 - 避免传输超大对象,尽量传必要字段。
- 跨来源通信时请在适配器内严格校验
origin。 - 作为跨系统或跨上下文协议时,事件名和消息字段建议使用
string,不要使用Symbol。 - 在页面卸载或模块销毁时调用
close()释放资源。
源码
查看完整项目源码 GitHub。
WebpageChannel 实现
import type {
IChannelData,
IErrorEvent,
IMessageErrorEvent,
IWebpageChannelAdapter
} from '../types';
import BroadcastChannelAdapter from './broadcast-channel-adapter';
import EventBus from './event-bus';
export default class WebpageChannel<
T extends Record<string, (args: any) => void>
> {
private channelName: string;
private eventBus: EventBus<T>;
private adapter: IWebpageChannelAdapter | null;
onError?: IErrorEvent;
onMessageError?: IMessageErrorEvent;
serializeMessage: (
data: IChannelData<Parameters<T[keyof T]>, keyof T>
) => string;
deserializeMessage: (
data: string
) => IChannelData<Parameters<T[keyof T]>, keyof T>;
constructor(
channelName: string,
options?: {
onError?: IErrorEvent;
onMessageError?: IMessageErrorEvent;
serializeMessage?: (
data: IChannelData<Parameters<T[keyof T]>, keyof T>
) => string;
deserializeMessage?: (
data: string
) => IChannelData<Parameters<T[keyof T]>, keyof T>;
},
adapter?: IWebpageChannelAdapter
) {
this.channelName = channelName;
this.eventBus = new EventBus<T>({
onListenerError: (error) => {
this.onError?.(error);
}
});
this.adapter = adapter ?? new BroadcastChannelAdapter(channelName);
this.onMessage();
this.onError = options?.onError;
this.onMessageError = options?.onMessageError;
this.adapter.onMessageError((e) => {
if (!this.onMessageError) return;
this.onMessageError(e);
});
this.serializeMessage = options?.serializeMessage ?? JSON.stringify;
this.deserializeMessage = options?.deserializeMessage ?? JSON.parse;
}
on<K extends keyof T>(event: K, callback: T[K]) {
this.eventBus.on(event, callback);
}
once<K extends keyof T>(event: K, callback: T[K]) {
this.eventBus.once(event, callback);
}
emit<K extends keyof T>(
event: K,
...[args]: Parameters<T[K]>[0] extends undefined
? []
: [Parameters<T[K]>[0]]
) {
const channelName = this.channelName;
const msg: IChannelData<Parameters<T[K]>, K> = {
channelName,
event,
data: args
};
return this.postMessage(msg);
}
off<K extends keyof T>(event: K, listener?: T[K]) {
this.eventBus.off(event, listener);
}
clear() {
this.eventBus.clear();
}
close() {
this.clear();
this.adapter?.close();
this.adapter = null;
}
private postMessage(data: IChannelData<Parameters<T[keyof T]>, keyof T>) {
if (!this.adapter) {
const error = new Error('Adapter is not initialized');
this.onError && this.onError(error);
return false;
}
try {
const message = this.serializeMessage(data);
this.adapter?.postMessage(message);
return true;
} catch (e: any) {
if (!(e instanceof Error)) {
e = new Error(e);
}
this.onError && this.onError(e);
return false;
}
}
private onMessage() {
this.adapter?.onMessage((message) => {
let res: IChannelData<Parameters<T[keyof T]>, keyof T>;
try {
res = this.deserializeMessage(message);
} catch (e: any) {
if (!(e instanceof Error)) {
e = new Error(e);
}
this.onError && this.onError(e);
return;
}
const key = res.event;
if (
res.channelName !== this.channelName ||
key === undefined ||
key === null
) {
return;
}
// @ts-ignore
this.eventBus.emit(key, res.data);
});
}
}
Event Bus 实现
type EventMap = Record<string, (args: any) => void>;
const ORIGINAL_LISTENER = Symbol('originalListener');
type WrappedListener<F extends (...args: any[]) => any> = F & {
[ORIGINAL_LISTENER]?: F;
};
export default class EventBus<T extends EventMap> {
private listeners: Partial<{ [K in keyof T]: T[K][] }> = {};
private onListenerError?: (error: Error) => void;
constructor(options?: { onListenerError?: (error: Error) => void }) {
this.onListenerError = options?.onListenerError;
}
on<K extends keyof T>(event: K, callback: T[K]) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
once<K extends keyof T>(event: K, callback: T[K]) {
const onceCallback = ((args: Parameters<T[K]>[0]) => {
this.off(event, onceCallback as T[K]);
callback(args);
}) as WrappedListener<T[K]>;
onceCallback[ORIGINAL_LISTENER] = callback;
this.on(event, onceCallback as T[K]);
}
emit<K extends keyof T>(
event: K,
...[args]: Parameters<T[K]>[0] extends undefined
? []
: [Parameters<T[K]>[0]]
) {
const callbacks = this.listeners[event];
if (!callbacks?.length) {
return;
}
[...callbacks].forEach((callback) => {
try {
callback(args);
} catch (e: unknown) {
const error = e instanceof Error ? e : new Error(String(e));
this.onListenerError?.(error);
}
});
}
off<K extends keyof T>(event: K, listener?: T[K]) {
if (!listener) {
delete this.listeners[event];
return;
}
const fns = this.listeners[event];
if (!fns?.length) {
return;
}
const idx = fns.findIndex((fn) => {
const wrapped = fn as WrappedListener<T[K]>;
return fn === listener || wrapped[ORIGINAL_LISTENER] === listener;
});
if (idx !== -1) {
fns.splice(idx, 1);
}
}
clear() {
this.listeners = {};
}
}
BroadcastChannelAdapter 实现
import type { IWebpageChannelAdapter } from '../types';
export default class BroadcastChannelAdapter implements IWebpageChannelAdapter {
private channel: BroadcastChannel;
constructor(channelName: string) {
this.channel = new BroadcastChannel(channelName);
}
postMessage(message: string) {
this.channel.postMessage(message);
}
onMessage(callback: (message: string) => void) {
this.channel.onmessage = (e) => {
callback(e.data);
};
}
onMessageError(callback: (e: MessageEvent) => void) {
this.channel.onmessageerror = callback;
}
close() {
this.channel.close();
}
}
PostMessageAdapter 实现
import type { IWebpageChannelAdapter } from '../types';
export default class PostMessageAdapter implements IWebpageChannelAdapter {
private targetWindow: Window;
private targetOrigin: string;
private messageHandler: ((e: MessageEvent) => void) | null = null;
private messageErrorHandler: ((e: MessageEvent) => void) | null = null;
constructor(targetWindow: Window, targetOrigin: string) {
this.targetWindow = targetWindow;
this.targetOrigin = targetOrigin;
}
postMessage(message: string) {
this.targetWindow.postMessage(message, this.targetOrigin);
}
onMessage(callback: (message: string) => void) {
if (this.messageHandler) {
window.removeEventListener('message', this.messageHandler);
}
this.messageHandler = (e) => {
if (e.origin === this.targetOrigin && e.source === this.targetWindow) {
callback(e.data);
}
};
window.addEventListener('message', this.messageHandler);
}
onMessageError(callback: (e: MessageEvent) => void) {
if (this.messageErrorHandler) {
window.removeEventListener('messageerror', this.messageErrorHandler);
}
this.messageErrorHandler = callback;
window.addEventListener('messageerror', this.messageErrorHandler);
}
close() {
if (this.messageHandler) {
window.removeEventListener('message', this.messageHandler);
this.messageHandler = null;
}
if (this.messageErrorHandler) {
window.removeEventListener('messageerror', this.messageErrorHandler);
this.messageErrorHandler = null;
}
}
}
类型定义
export interface IWebpageChannelAdapter {
postMessage(message: string): void;
onMessage(callback: (message: string) => void): void;
onMessageError(callback: (e: MessageEvent) => void): void;
close(): void;
}
export interface IChannelData<T = any, C = string> {
channelName: string
event?: C;
data?: T;
}
export type IErrorEvent = (e: Error) => void;
export type IMessageErrorEvent = (e: MessageEvent) => void;