webpage-channel 让不同页面通信像组件通信一样简便

一个轻量级、类型友好的浏览器端消息通信库。

简介

提供统一的事件 API(ononceemitoff等),用于在不同网页上下文之间通信,例如:多标签页、iframe 与 worker 场景。也可以用于跨组件通信。

默认基于 BroadcastChannel,并支持通过适配器扩展到 postMessage 等通信方式。

代码实现不到 200 行,单元测试覆盖率 100%

特性

  • 轻量易用:ononceemitoff 即可完成事件收发。
  • TypeScript 友好:通过 泛型 约束事件名和事件数据类型。
  • 可扩展适配器:默认 BroadcastChannel,可自定义适配器。
  • 可自定义序列化:支持替换 JSON.stringify/parse
  • 错误可观察:提供消息编解码错误与底层消息错误回调。
  • 独立事件系统:抽离了 Event bus 实现,可以只引入该模块,用于组件通信等。

安装

bash 复制代码
pnpm add webpage-channel
# 或
npm i webpage-channel
# 或
yarn add webpage-channel

同域通信

默认基于 BroadcastChannel,同域页面通信时,可以直接使用默认配置。跨域通信请参考跨域通信

1. 定义事件类型

在同域的两个不同页面,都添加以下代码:

ts 复制代码
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. 监听消息

在接收消息方,添加以下代码:

ts 复制代码
channel.on('user:update', (payload) => {
	console.log('收到用户更新', payload.id, payload.name);
});

3. 发送消息

在发送消息方,添加以下代码:

ts 复制代码
const ok = channel.emit('user:update', { id: 'u1', name: 'Alice' });
if (!ok) {
	console.warn('消息发送失败');
}

4. 取消监听和销毁(可选)

ts 复制代码
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.contentWindowwindow.parent)。
  • targetOrigin: string:目标来源(例如 https://example.com,或开发环境 *)。

1. 父页面发送消息给 iframe

ts 复制代码
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 接收消息

ts 复制代码
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 === targetOrigine.source === targetWindow
  • WebpageChannel 的第一个参数 name ,父子页面必须一致,内部会校验,不一致则接收不到消息。

跨组件通信

该库抽离了 Event Bus 的实现,作为一个单独模块提供,因此可以只引入该模块,用于组件通信。

该模块提供了 ononceemitoffclear方法,WebpageChannel 内部使用的就是该模块。

1. 父组件

ts 复制代码
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

发送消息

ts 复制代码
import { bus } from 'parent'

bus.emit('ping', { value: 1 });

bus.emit('pong', { result: 'hello' })

3. 兄弟组件 B

接收消息

ts 复制代码
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.postMessageMessagePort 等)。

内置适配器

  • BroadcastChannelAdapter:默认适配器,适合同源多标签页/上下文通信。
  • PostMessageAdapter:适合父页面与 iframe、弹窗窗口等基于 window.postMessage 的场景。

自定义适配器示例

ts 复制代码
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());

序列化定制示例

ts 复制代码
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

相关推荐
mldong8 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排9 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby606110 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角11 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!11 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰11 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi12 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen13 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马14 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒14 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端