h5-iframe-ipc
Version:
一个轻量级的H5与iframe通信SDK,支持双向消息传递和事件监听
722 lines (543 loc) • 16.9 kB
Markdown
# h5-iframe-ipc
<div align="center">
[](https://www.npmjs.com/package/h5-iframe-ipc)
[](https://github.com/hhy164/ipc-sdk/blob/main/LICENSE)
[](https://www.npmjs.com/package/h5-iframe-ipc)
一个轻量级、类型安全的 H5 与 iframe 双向通信 SDK
</div>
## ✨ 特性
- 🚀 **轻量级**:零依赖,gzip 后仅 ~2KB
- 🔒 **类型安全**:完整的 TypeScript 类型定义
- 📡 **双向通信**:支持父页面 ↔ 子页面(H5)双向消息传递
- 🎯 **Promise 支持**:invoke 方法支持 Promise,轻松处理异步操作
- 📢 **事件系统**:内置事件监听和发射机制(on/off/emit)
- 🧹 **资源管理**:提供 destroy 方法,避免内存泄漏
- 🔧 **简单易用**:API 设计简洁直观,5 分钟快速上手
## 📦 安装
```bash
# npm
npm install h5-iframe-ipc
# yarn
yarn add h5-iframe-ipc
# pnpm
pnpm add h5-iframe-ipc
```
## 🚀 快速开始
### H5 子页面(在 iframe 内运行)
H5 页面使用 `IPCH5` 类,运行在 iframe 内部。
```javascript
import { IPCH5 } from 'h5-iframe-ipc';
// 创建 IPC 实例
const ipc = new IPCH5();
// 注册方法供父页面调用
ipc.handle('getUserInfo', (params) => {
return {
name: '张三',
age: 25,
email: 'zhangsan@example.com'
};
});
// 监听来自父页面的事件
ipc.on('parentReady', (data) => {
console.log('父页面已就绪:', data);
});
// 向父页面发送事件
ipc.emit('h5Loaded', {
timestamp: Date.now(),
version: '1.0.0'
});
```
### iframe 父页面(包含 iframe 标签)
父页面使用 `IPCIframe` 类,需要传入 iframe 的 contentWindow。
```javascript
import { IPCIframe } from 'h5-iframe-ipc';
// 获取 iframe 元素
const iframe = document.getElementById('myIframe');
// 创建 IPC 实例(传入 iframe 的 contentWindow)
const ipc = new IPCIframe(iframe.contentWindow);
// 调用 H5 注册的方法(支持 async/await)
async function fetchUserInfo() {
try {
const userInfo = await ipc.invoke('getUserInfo', {
userId: '12345'
});
console.log('用户信息:', userInfo);
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
// 监听来自 H5 的事件
ipc.on('h5Loaded', (data) => {
console.log('H5 页面已加载:', data);
});
// 向 H5 发送事件
ipc.emit('parentReady', {
status: 'ready',
version: '1.0.0'
});
```
## 📖 API 文档
### IPCH5
H5 子页面(运行在 iframe 内)使用的 IPC 实例。
#### 构造函数
```typescript
const ipc = new IPCH5();
```
无需传参,自动通过 `window.parent` 与父页面通信。
#### 方法
##### `handle(channel: string, callback: Function): void`
注册一个方法,供父页面通过 `invoke` 调用。
**参数:**
- `channel` - 方法通道名称
- `callback` - 回调函数,可以返回任意值或 Promise
**示例:**
```javascript
// 同步返回
ipc.handle('getConfig', (params) => {
return { theme: 'dark', language: 'zh-CN' };
});
// 异步返回(Promise)
ipc.handle('fetchData', async (params) => {
const response = await fetch('/api/data');
return response.json();
});
```
##### `on(eventName: string, callback: Function): void`
监听来自父页面的事件。
**参数:**
- `eventName` - 事件名称
- `callback` - 事件处理函数
**示例:**
```javascript
ipc.on('themeChanged', (data) => {
console.log('主题已切换:', data);
});
```
##### `off(eventName: string, callback?: Function): void`
移除事件监听。
**参数:**
- `eventName` - 事件名称
- `callback` - (可选)要移除的具体回调函数,不传则移除该事件的所有监听
**示例:**
```javascript
const handler = (data) => console.log(data);
// 添加监听
ipc.on('myEvent', handler);
// 移除特定监听
ipc.off('myEvent', handler);
// 移除该事件的所有监听
ipc.off('myEvent');
```
##### `emit(eventName: string, params: any): void`
向父页面发送事件。
**参数:**
- `eventName` - 事件名称
- `params` - 事件数据
**示例:**
```javascript
ipc.emit('userAction', {
type: 'click',
target: 'submitButton'
});
```
##### `destroy(): void`
销毁 IPC 实例,清理所有监听器和处理器。
**示例:**
```javascript
// 页面卸载时清理
window.addEventListener('beforeunload', () => {
ipc.destroy();
});
```
---
### IPCIframe
iframe 父页面使用的 IPC 实例。
#### 构造函数
```typescript
const iframe = document.getElementById('myIframe');
const ipc = new IPCIframe(iframe.contentWindow);
```
**参数:**
- `win` - iframe 的 contentWindow 对象
**示例:**
```javascript
// 方式一:通过 getElementById
const iframe = document.getElementById('myIframe');
const ipc = new IPCIframe(iframe.contentWindow);
// 方式二:通过 querySelector
const iframe = document.querySelector('#myIframe');
const ipc = new IPCIframe(iframe.contentWindow);
// 方式三:监听 iframe 加载后初始化
const iframe = document.getElementById('myIframe');
iframe.onload = () => {
const ipc = new IPCIframe(iframe.contentWindow);
};
```
#### 方法
##### `invoke(channel: string, params: any): Promise<any>`
调用 H5 注册的方法,返回 Promise。
**参数:**
- `channel` - 方法通道名称
- `params` - 传递的参数
**返回:**
- `Promise<any>` - 返回 H5 处理函数的结果
**示例:**
```javascript
// 基本用法
const result = await ipc.invoke('getData', { id: 123 });
// 错误处理
try {
const user = await ipc.invoke('getUserInfo', { userId: '456' });
console.log(user);
} catch (error) {
console.error('调用失败:', error);
}
```
##### `on(eventName: string, callback: Function): void`
监听来自 H5 的事件。
**参数:**
- `eventName` - 事件名称
- `callback` - 事件处理函数
**示例:**
```javascript
ipc.on('h5Ready', (data) => {
console.log('H5 已就绪:', data);
});
```
##### `off(eventName: string, callback?: Function): void`
移除事件监听。
**参数:**
- `eventName` - 事件名称
- `callback` - (可选)要移除的具体回调函数
**示例:**
```javascript
const handler = (data) => console.log(data);
// 添加监听
ipc.on('update', handler);
// 移除特定监听
ipc.off('update', handler);
// 移除所有监听
ipc.off('update');
```
##### `emit(eventName: string, params: any): void`
向 H5 发送事件。
**参数:**
- `eventName` - 事件名称
- `params` - 事件数据
**示例:**
```javascript
ipc.emit('notification', {
type: 'success',
message: '操作成功'
});
```
##### `destroy(): void`
销毁 IPC 实例,清理所有监听器和待处理的 Promise。
**示例:**
```javascript
// 组件卸载时清理
componentWillUnmount() {
ipc.destroy();
}
```
## 🎯 完整示例
### 场景:用户登录流程
#### H5 子页面(login.html - 运行在 iframe 内)
```javascript
import { IPCH5 } from 'h5-iframe-ipc';
class LoginPage {
constructor() {
this.ipc = new IPCH5();
this.init();
}
init() {
// 注册登录方法供父页面调用
this.ipc.handle('login', async (credentials) => {
const { username, password } = credentials;
// 调用后端 API
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (!response.ok) {
throw new Error('登录失败');
}
const result = await response.json();
// 通知父页面登录成功
this.ipc.emit('loginSuccess', result.user);
return result;
});
// 注册获取配置方法
this.ipc.handle('getLoginConfig', () => {
return {
enableSocialLogin: true,
supportedProviders: ['wechat', 'alipay'],
locale: 'zh-CN'
};
});
// 监听父页面的主题变化
this.ipc.on('themeChanged', (theme) => {
console.log('主题已切换:', theme);
this.applyTheme(theme);
});
// 监听初始数据
this.ipc.on('initData', (data) => {
console.log('接收到初始数据:', data);
this.applyTheme(data.theme);
});
// 通知父页面 H5 已就绪
this.ipc.emit('h5Ready', {
version: '1.0.0',
timestamp: Date.now()
});
}
applyTheme(theme) {
document.body.setAttribute('data-theme', theme);
}
destroy() {
this.ipc.destroy();
}
}
// 创建实例
const loginPage = new LoginPage();
// 绑定表单提交
document.querySelector('#loginForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.querySelector('#username').value;
const password = document.querySelector('#password').value;
// 可以在 H5 内部直接调用注册的方法,也可以等父页面调用
try {
const result = await loginPage.ipc.invoke('login', { username, password });
console.log('登录成功:', result);
} catch (error) {
console.error('登录失败:', error);
}
});
```
#### iframe 父页面(index.html - 包含 iframe)
```html
<!DOCTYPE html>
<html>
<head>
<title>父页面</title>
</head>
<body>
<!-- iframe 嵌入 H5 页面 -->
<iframe id="loginIframe" src="login.html" width="100%" height="600px"></iframe>
<script type="module">
import { IPCIframe } from 'h5-iframe-ipc';
class ParentPage {
constructor() {
this.iframe = document.getElementById('loginIframe');
this.initIframe();
}
initIframe() {
// 等待 iframe 加载完成
this.iframe.onload = () => {
// 创建 IPC 实例
this.ipc = new IPCIframe(this.iframe.contentWindow);
this.init();
};
}
async init() {
// 获取 H5 的配置
const config = await this.ipc.invoke('getLoginConfig', {});
console.log('H5 配置:', config);
// 监听 H5 就绪事件
this.ipc.on('h5Ready', (data) => {
console.log('H5 已就绪:', data);
// 向 H5 发送初始数据
this.ipc.emit('initData', {
theme: 'light',
apiUrl: 'https://api.example.com'
});
});
// 监听登录成功事件
this.ipc.on('loginSuccess', (user) => {
console.log('用户登录成功:', user);
this.updateUserInfo(user);
});
// 可以从父页面主动调用 H5 的登录方法
this.bindParentLoginButton();
}
bindParentLoginButton() {
document.getElementById('parentLoginBtn')?.addEventListener('click', async () => {
try {
const result = await this.ipc.invoke('login', {
username: 'testuser',
password: 'password123'
});
console.log('通过父页面调用登录成功:', result);
} catch (error) {
console.error('登录失败:', error);
}
});
}
updateUserInfo(user) {
// 更新父页面 UI
document.getElementById('username').textContent = user.name;
}
changeTheme(theme) {
// 通知 H5 切换主题
this.ipc.emit('themeChanged', theme);
}
destroy() {
this.ipc.destroy();
}
}
// 创建实例
const parentPage = new ParentPage();
</script>
</body>
</html>
```
## 🔧 TypeScript 支持
本 SDK 使用 TypeScript 编写,提供完整的类型定义。
```typescript
import { IPCH5, IPCIframe, IPCType } from 'h5-iframe-ipc';
// H5 子页面
const h5Ipc: IPCH5 = new IPCH5();
// 自定义类型
interface UserInfo {
name: string;
age: number;
email: string;
}
h5Ipc.handle('getUserInfo', async (): Promise<UserInfo> => {
return {
name: '张三',
age: 25,
email: 'zhangsan@example.com'
};
});
// iframe 父页面
const iframe = document.getElementById('myIframe') as HTMLIFrameElement;
const parentIpc: IPCIframe = new IPCIframe(iframe.contentWindow!);
// 调用时可以指定返回类型
const userInfo = await parentIpc.invoke<UserInfo>('getUserInfo', {});
console.log(userInfo.name); // TypeScript 会有类型提示
```
## ⚠️ 注意事项
### 1. iframe 加载时机
必须等待 iframe 加载完成后再创建 `IPCIframe` 实例:
```javascript
// ✅ 正确
const iframe = document.getElementById('myIframe');
iframe.onload = () => {
const ipc = new IPCIframe(iframe.contentWindow);
};
// ❌ 错误 - iframe 可能还未加载完成
const iframe = document.getElementById('myIframe');
const ipc = new IPCIframe(iframe.contentWindow); // contentWindow 可能为 null
```
### 2. 内存泄漏
在组件或页面销毁时,务必调用 `destroy()` 方法清理资源:
```javascript
// React 示例
useEffect(() => {
const iframe = document.getElementById('myIframe');
let ipc;
iframe.onload = () => {
ipc = new IPCIframe(iframe.contentWindow);
};
return () => {
ipc?.destroy(); // 清理
};
}, []);
// Vue 3 示例
import { onMounted, onUnmounted } from 'vue';
let ipc = null;
onMounted(() => {
const iframe = document.getElementById('myIframe');
iframe.onload = () => {
ipc = new IPCIframe(iframe.contentWindow);
};
});
onUnmounted(() => {
ipc?.destroy();
});
```
### 3. 错误处理
`invoke` 方法返回 Promise,务必处理可能的错误:
```javascript
try {
const result = await ipc.invoke('someMethod', params);
// 处理成功结果
} catch (error) {
console.error('调用失败:', error);
// 错误处理逻辑
}
```
### 4. 事件监听器管理
避免重复添加相同的监听器,必要时使用 `off` 方法移除:
```javascript
const handler = (data) => console.log(data);
// 添加前先移除(确保不会重复添加)
ipc.off('myEvent', handler);
ipc.on('myEvent', handler);
```
### 5. IPCIframe 构造函数参数
创建 `IPCIframe` 实例时,必须传入 iframe 的 contentWindow:
```javascript
// ✅ 正确
const iframe = document.getElementById('myIframe');
const ipc = new IPCIframe(iframe.contentWindow);
// ❌ 错误
const ipc = new IPCIframe(window); // 这是错误的
const ipc = new IPCIframe(window.parent); // 这也是错误的
```
## 🏗️ 架构说明
```
┌─────────────────────────────────────┐
│ iframe 父页面 (Parent) │
│ │
│ import { IPCIframe } │
│ const ipc = new IPCIframe( │
│ iframe.contentWindow │
│ ) │
│ │
│ ┌─────────────────────────────┐ │
│ │ <iframe src="h5.html"> │ │
│ │ │ │
│ │ H5 子页面 (Child) │ │
│ │ │ │
│ │ import { IPCH5 } │ │
│ │ const ipc = new IPCH5() │ │
│ │ │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────┘
通信流程:
父页面 → ipc.invoke() → H5.handle()
父页面 ← H5.emit() ← H5 子页面
父页面 → ipc.emit() → H5.on()
父页面 ← ipc.on() ← H5.emit()
```
## 🌐 浏览器兼容性
支持所有支持 `postMessage` API 的现代浏览器:
- ✅ Chrome / Edge (Chromium) >= 60
- ✅ Firefox >= 55
- ✅ Safari >= 11
- ✅ iOS Safari >= 11
- ✅ Android WebView >= 60
## 📚 相关文档
- [postMessage API](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage)
- [HTMLIFrameElement.contentWindow](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLIFrameElement/contentWindow)
- [iframe 通信最佳实践](https://web.dev/cross-window-communication/)
## 🐛 问题反馈
如果你发现任何问题或有改进建议,欢迎提交 Issue:
- [GitHub Issues](https://github.com/hhy164/ipc-sdk/issues)
## 🤝 贡献
欢迎提交 Pull Request!在提交之前,请确保:
1. 代码通过 TypeScript 编译
2. 遵循项目代码风格
3. 添加必要的注释
## 📄 许可证
[MIT](./LICENSE) © hehongyu
## 🔗 相关链接
- [npm 包地址](https://www.npmjs.com/package/h5-iframe-ipc)
- [GitHub 仓库](https://github.com/hhy164/ipc-sdk)
- [问题反馈](https://github.com/hhy164/ipc-sdk/issues)
---
如果这个项目对你有帮助,欢迎 ⭐ Star 支持一下!