feat(iframe-bridge): implement iframe communication bridge and language sync

This commit is contained in:
2025-11-14 20:07:08 +08:00
parent 4356813820
commit 034c190373
15 changed files with 469 additions and 160 deletions

View File

@@ -0,0 +1,141 @@
# @teres/iframe-bridge
一个基于 [Penpal](https://github.com/Aaronius/penpal) 的轻量 iframe 通信桥帮助宿主页面与子应用iframe安全、稳定地进行双向方法调用与状态同步。
## 为什么需要它
- 将跨项目的交互(如语言切换、导航、关闭、就绪通知)抽象为清晰的 API。
- 在 Monorepo 或多项目场景下,提供“宿主 ↔ 子应用”的统一通信约定。
- 屏蔽 Penpal 细节,专注业务方法与类型定义。
## 安装与版本
- 已在工作空间内作为包使用(`packages/iframe-bridge`)。
- 依赖:`penpal@^6.2.1`
## 目录结构
```
packages/iframe-bridge/
├── package.json
├── README.md
└── src/
├── index.ts # 入口聚合与导出(不含业务逻辑)
├── types.ts # 对外类型HostApi、ChildApi
├── path.ts # 路径转换与是否嵌入判断
├── client.ts # 子端到父端的连接与客户端方法
├── host.ts # 宿主侧创建与管理与子端的连接
└── logger.ts # 统一日志工具
```
## 核心概念
- Host宿主包含 `iframe` 的父页面。
- Child子端被嵌入在 `iframe` 中的子应用。子端暴露方法给宿主调用。
- Penpal在宿主与子端之间建立安全的双向 RPC方法调用连接。
## 快速上手
### 宿主侧Host示例创建连接并缓存子端引用
```ts
import { createPenpalHostBridge } from '@teres/iframe-bridge';
// iframeRef 指向你页面中的 <iframe>
const { child, destroy } = await createPenpalHostBridge({
iframe: iframeRef.current!,
methods: {
// 暴露给“子端”可调用的宿主方法
navigate: (path: string) => {
// 宿主导航逻辑
},
close: () => {
// 关闭或返回逻辑
},
agentReady: (agentId?: string) => {
// 子端就绪通知处理
},
},
});
// child 是一个 Promise解析后可拿到子端暴露的方法例如 changeLanguage
await child; // 也可以缓存后续使用
// 组件卸载或需要重建连接时
await destroy();
```
在你的项目中(如 `src/pages/ragflow/iframe.tsx`)通常会把 `child` 引用存到一个管理器,供语言切换等场景调用。
### 子端侧Child示例暴露方法并与宿主交互
```ts
import Bridge from '@teres/iframe-bridge';
// 在子应用初始化时调用(例如根组件 useEffect 中)
Bridge.initChildBridge({
// 暴露给“宿主”可调用的子端方法
methods: {
changeLanguage: async (lang: string) => {
// 你的 i18n 切换逻辑,例如:
// await i18n.changeLanguage(lang);
},
},
});
// 主动与宿主交互(可选)
Bridge.clientReady();
Bridge.clientNavigate('/some/path');
Bridge.clientClose();
```
## API 参考
### 类型types.ts
- `HostApi`
- `navigate(path: string): void`
- `close(): void`
- `agentReady(agentId?: string): void`
- `ChildApi`
- `changeLanguage(lang: string): Promise<void>`
### 路径与嵌入path.ts
- `isEmbedded(): boolean` 判断当前窗口是否运行在 iframe 中(且需要通信)。
- `toHostPath(childPath: string): string` 将子端路径转换为宿主侧路由前缀路径。
- `toChildPath(hostPath: string): string` 将宿主路径映射回子端路径。
### 子端客户端方法client.ts
- `getClientHostApi(): Promise<HostApi>` 建立并缓存与宿主的连接。
- `clientNavigate(path: string): Promise<void>` 请求宿主导航。
- `clientClose(): Promise<void>` 请求宿主关闭或返回。
- `clientReady(agentId?: string): Promise<void>` 通知宿主子端就绪。
- `initChildBridge(options: { methods: Partial<ChildApi> }): Promise<void>` 在子端暴露方法供宿主调用(如 `changeLanguage`)。
### 宿主桥接host.ts
- `createPenpalHostBridge({ iframe, methods })`
- `iframe: HTMLIFrameElement`
- `methods: Partial<HostApi>` 暴露给子端可调用的宿主方法。
- 返回:`{ child: Promise<ChildExposed>, destroy: () => Promise<void> }`
- `child`:解析后可获得子端暴露的方法(如 `changeLanguage`)。
- `destroy`:销毁连接与事件监听。
## 语言联动最佳实践
1. 子端在初始化中通过 `initChildBridge` 暴露 `changeLanguage`
2. 宿主在创建连接后缓存 `child` 引用。
3. 宿主语言切换组件在变更时调用:
```ts
const child = await childPromise; // 取到缓存的子端引用
await child.changeLanguage(nextLang);
```
## 调试与排错
- 确保 iframe 与宿主同源或允许跨源通信Penpal 支持跨源,但需正确 URL
- 连接建立需要子端加载完成;在子端 `initChildBridge` 之前调用子端方法会报错或超时。
- 若需要观察连接过程,可在 `logger.ts` 中启用调试日志或在业务代码中打点。
- 组件卸载时务必调用 `destroy()`,避免内存泄漏或事件残留。
## 设计说明
- 入口 `index.ts` 仅做导出聚合,具体逻辑按职责拆分到 `types/logger/path/client/host`
- 子端侧会缓存一次宿主连接,避免重复握手,提高性能与稳定性。
## 常见问题
- 调用时机过早:请在子端完成初始化(`initChildBridge`)后再由宿主调用子端方法。
- 路径映射:`toHostPath`/`toChildPath` 默认适配当前项目前缀,如需自定义可扩展配置模块。
- 异步方法:所有跨端调用均为异步,注意 `await` 与错误处理。
---
若你需要把路径前缀改为可配置项或扩展更多方法(如主题切换、会话同步),欢迎提 issue 或继续迭代。