npm package: @siact/sime-bridge
Description: 用于在父容器页面与嵌入的 iframe 之间建立双向指令通信的轻量桥接库。基于 [Comlink](https://github.com/GoogleChromeLabs/comlink) 的 RPC 机制，通过 `HostBridge` 和 `ClientBridge`，宿主应用可以暴露受控的命令，iframe 客户端可发现并调用这些命令，实现对父容器的安全控制。
Latest version: 0.0.1

# Si-Me Bridge

用于在父容器页面与嵌入的 iframe 之间建立双向指令通信的轻量桥接库。基于 [Comlink](https://github.com/GoogleChromeLabs/comlink) 的 RPC 机制，通过 `HostBridge` 和 `ClientBridge`，宿主应用可以暴露受控的命令，iframe 客户端可发现并调用这些命令，实现对父容器的安全控制。

## 安装

```bash
pnpm add @siact/sime-bridge
# 或
npm install @siact/sime-bridge
```

## 基本原理

```
┌─────────────────────────────┐
│   宿主页面 (Host)           │
│  ┌───────────────────────┐  │
│  │   HostBridge          │  │
│  │  - expose()           │  │
│  │  - CommandProvider    │  │
│  └───────────────────────┘  │
└────────────┬────────────────┘
             │ postMessage
             │ (Comlink RPC)
             │
┌────────────┴────────────────┐
│   iframe 客户端 (Client)    │
│  ┌───────────────────────┐  │
│  │   ClientBridge        │  │
│  │  - wrap()             │  │
│  │  - discover()         │  │
│  │  - execute()          │  │
│  └───────────────────────┘  │
└─────────────────────────────┘
```

## 快速开始

### 宿主页面（父容器）

```typescript
import { HostBridge } from '@siact/sime-bridge';

const hostBridge = new HostBridge({
  iframe: document.querySelector('#app-iframe'),
  allowedOrigins: ['https://your-client-origin'],
  debug: true,
});

hostBridge.register({
  project: 'host-demo',
  description: '父容器控制指令',
  version: '1.0.0',
  commands: [
    {
      name: 'open-modal',
      description: '在宿主中打开一个弹窗',
      handler: (title: string) => {
        console.log(`打开弹窗: ${title}`);
        return { success: true };
      },
    },
    {
      name: 'navigate',
      description: '切换宿主路由',
      handler: async (path: string) => {
        console.log(`导航到: ${path}`);
        return { success: true, path };
      },
    },
    {
      name: 'get-user-info',
      description: '获取当前用户信息',
      handler: async () => {
        return {
          id: 'user-123',
          name: '张三',
          role: 'admin',
        };
      },
    },
  ],
});

// 页面卸载时清理资源
window.addEventListener('beforeunload', () => hostBridge.destroy());
```

### iframe 客户端

```typescript
import { ClientBridge } from '@siact/sime-bridge';

const clientBridge = new ClientBridge({
  targetOrigin: 'https://your-host-origin',
  debug: true,
});

async function init() {
  // 发现宿主提供的命令
  const provider = await clientBridge.discover();
  console.log('可用命令:', provider.commands);

  // 执行宿主命令
  const userInfo = await clientBridge.execute('get-user-info');
  console.log('用户信息:', userInfo);

  await clientBridge.execute('open-modal', ['来自 iframe 的弹窗']);
  await clientBridge.execute('navigate', ['/dashboard']);
}

init();

window.addEventListener('beforeunload', () => clientBridge.destroy());
```

### 让客户端向宿主暴露命令

从 `vX.Y.Z` 起，客户端也可以通过 `clientBridge.register()` 注册自身命令，供宿主发现与调用：

```typescript
import { ClientBridge } from '@siact/sime-bridge';

const clientBridge = new ClientBridge({
  targetOrigin: 'https://your-host-origin',
  debug: true,
});

clientBridge.register({
  project: 'client-demo',
  description: 'iframe 暴露给宿主的能力',
  version: '1.0.0',
  commands: [
    {
      name: 'get-client-state',
      description: '返回 iframe 内部的会话状态',
      handler: () => {
        return { theme: 'dark', unreadCount: 2 };
      },
    },
    {
      name: 'focus-input',
      description: '聚焦聊天输入框',
      handler: () => {
        const input = document.querySelector<HTMLInputElement>('#chat-input');
        input?.focus();
        return { focused: !!input };
      },
    },
  ],
});
```

宿主侧可调用新增的 `discoverClient()` 和 `executeOnClient()` 方法：

```typescript
import { HostBridge } from '@siact/sime-bridge';

const hostBridge = new HostBridge({
  iframe: document.querySelector('#app-iframe'),
  allowedOrigins: ['https://your-client-origin'],
  debug: true,
});

async function useClientCommands() {
  const clientProvider = await hostBridge.discoverClient();
  console.log('客户端暴露的命令:', clientProvider.commands);

  const clientState = await hostBridge.executeOnClient('get-client-state');
  console.log('客户端状态:', clientState);

  await hostBridge.executeOnClient('focus-input');
}

useClientCommands();
```

## 配置说明

### HostBridge 选项

- **`iframe`** (可选): 指定目标 iframe 元素。如果不指定，Comlink 会自动使用 `window.parent`。
- **`allowedOrigins`** (可选): 允许接收消息的来源，默认 `['*']`。**推荐在生产环境中填写具体域名以提高安全性**。
- **`debug`** (可选): 启用调试模式，在控制台输出通信日志。

### ClientBridge 选项

- **`targetWindow`** (可选): 目标窗口对象，默认 `window.parent`。
- **`targetOrigin`** (可选): 宿主页面的 origin，默认 `'*'`。**推荐在生产环境中指定具体 origin**。
- **`debug`** (可选): 启用调试模式。

## 高级用法

### 错误处理

```typescript
async function executeWithErrorHandling() {
  try {
    const result = await clientBridge.execute('non-existent-command');
  } catch (error) {
    if (error instanceof Error) {
      console.error('错误信息:', error.message);
      // 输出: 错误信息: Command "non-existent-command" not found
    }
  }
}
```

### 缓存 discover 结果

`ClientBridge` 会自动缓存第一次 `discover()` 的结果，后续调用会直接返回缓存：

```typescript
const provider1 = await clientBridge.discover(); // 发送请求到宿主
const provider2 = await clientBridge.discover(); // 返回缓存结果
```

### 支持复杂数据类型

Comlink 支持传递和返回复杂的数据类型（对象、数组、Promise 等）：

```typescript
// 宿主侧
{
  name: "get-data",
  handler: async () => {
    return {
      users: [
        { id: 1, name: "Alice" },
        { id: 2, name: "Bob" },
      ],
      metadata: {
        total: 2,
        timestamp: new Date(),
      },
    };
  },
}

// 客户端侧
const data = await clientBridge.execute("get-data");
console.log(data.users); // [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
```

## 完整示例

### 宿主页面 (host.html)

```html
<!DOCTYPE html>
<html>
  <head>
    <title>宿主页面</title>
  </head>
  <body>
    <h1>宿主页面</h1>
    <iframe id="app-iframe" src="http://localhost:3000/client.html"></iframe>

    <script type="module">
      import { HostBridge } from '@siact/sime-bridge';

      const hostBridge = new HostBridge({
        iframe: document.querySelector('#app-iframe'),
        debug: true,
      });

      hostBridge.register({
        project: 'host-app',
        version: '1.0.0',
        commands: [
          {
            name: 'alert',
            handler: (message) => {
              alert(message);
              return { shown: true };
            },
          },
        ],
      });

      window.addEventListener('beforeunload', () => hostBridge.destroy());
    </script>
  </body>
</html>
```

### 客户端页面 (client.html)

```html
<!DOCTYPE html>
<html>
  <head>
    <title>客户端页面</title>
  </head>
  <body>
    <h1>客户端页面</h1>
    <button id="btn">点击我</button>

    <script type="module">
      import { ClientBridge } from '@siact/sime-bridge';

      const clientBridge = new ClientBridge({
        debug: true,
      });

      document.getElementById('btn').addEventListener('click', async () => {
        const result = await clientBridge.execute('alert', ['来自 iframe 的问候！']);
        console.log(result);
      });

      window.addEventListener('beforeunload', () => clientBridge.destroy());
    </script>
  </body>
</html>
```

## 类型定义

### CommandHandler

```typescript
type CommandHandler = (...args: any[]) => any | Promise<any>;
```

### CommandDefinition

```typescript
interface CommandDefinition {
  name: string;
  description?: string;
  handler: CommandHandler;
}
```

### DiscoveredProvider

```typescript
interface DiscoveredProvider {
  project: string;
  description?: string;
  version?: string;
  commands: DiscoveredCommand[];
}
```

## 最佳实践

### 安全性

- 在生产环境中，始终在 `allowedOrigins` 中指定具体的域名
- 在 `ClientBridge` 中指定 `targetOrigin` 而不是使用 `'*'`
- 只暴露必要的命令，避免暴露敏感操作

### 性能

- 利用 `discover()` 的自动缓存机制
- 避免频繁创建和销毁 Bridge 实例
- 使用 `timeoutMs` 设置合理的超时时间

### 调试

- 在开发环境启用 `debug: true` 以便追踪通信过程
- 在生产环境禁用 debug 模式以减少日志输出

### 资源清理

- 页面卸载时调用 `destroy()` 方法，避免内存泄漏
- 清理事件监听器和定时器

## 常见问题

**Q: 为什么使用 Comlink？**
A: Comlink 提供了一个简洁的 RPC 接口，自动处理 postMessage 通信的复杂性，使代码更易读易维护。

**Q: 支持哪些数据类型？**
A: Comlink 支持大多数可序列化的 JavaScript 类型，包括对象、数组、Date、Map、Set 等。不支持函数和循环引用。

**Q: 如何处理长时间运行的命令？**
A: 可以增加 `timeoutMs` 的值，或者在命令中使用 WebSocket 等实时通信方式来推送进度更新。

**Q: 能否在多个 iframe 之间通信？**
A: 可以。每个 iframe 创建独立的 `ClientBridge` 实例，连接到同一个宿主。

## 构建与发布

```bash
pnpm install
pnpm build
```

`pnpm build` 将生成 `dist/` 产物和类型声明，方便发布到私有或公共 npm 镜像。