# @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 或继续迭代。