npm package: @sl-material/sl-evolink
Description: 商龙通信进化 SDK - 微前端/iframe 应用间通信解决方案
Latest version: 1.0.0

# sl-evolink

商龙通信进化 SDK - 微前端/iframe 应用间通信解决方案

基于 [Comlink](https://github.com/GoogleChromeLabs/comlink) 封装，提供类型安全的 RPC 调用，支持 wujie、普通 iframe 等微前端场景。

## 特性

- 类型安全的远程方法调用
- 支持多实例管理（多 Tab 场景）
- 支持 Class 远程实例化
- 支持子应用间直连通信
- **同时兼容 wujie 和普通 iframe**（渐进式迁移友好）
- 兼容 wujie、qiankun、iframe 等微前端方案

## 安装

```bash
npm install sl-evolink
# 或
pnpm add sl-evolink
```

## 快速开始

### 1. 主应用（父页面）

```typescript
// main-app/src/bridge.ts
import { ComlinkBridge, AppType } from "sl-evolink";

// 定义子应用 API 类型（可选，用于类型提示）
interface ChildAppAPI {
  getStatus: () => Promise<{ appName: string; version: string }>;
  sendMessage: (msg: string) => Promise<{ success: boolean }>;
}

// 创建 Bridge 实例
export const bridge = new ComlinkBridge({
  appName: "main",
  appType: AppType.SLY,
  api: {
    // 暴露给子应用调用的方法
    async getConfig() {
      return { theme: "dark", lang: "zh-CN" };
    },
    async notify(message: string) {
      console.log("收到子应用消息:", message);
      return { success: true };
    },
  },
});

// 获取子应用代理并调用
async function callChildApp(instanceId?: string) {
  // 推荐：多 Tab 场景使用 instanceId 精确匹配
  const childApp = instanceId
    ? await bridge.getAppByInstanceId<ChildAppAPI>(instanceId)
    : await bridge.getApp<ChildAppAPI>("child-app");

  const status = await childApp.getStatus();
  console.log("子应用状态:", status);
}
```

### 2. 子应用（子页面）

```typescript
// child-app/src/bridge.ts
import { ComlinkBridge, AppType } from "sl-evolink";

// 创建 Bridge 实例
export const bridge = new ComlinkBridge({
  appName: "child-app",
  appType: AppType.CRM,
  api: {
    // 暴露给主应用调用的方法
    async getStatus() {
      return { appName: "child-app", version: "1.0.0" };
    },
    async sendMessage(msg: string) {
      console.log("收到消息:", msg);
      return { success: true };
    },
  },
});

// 调用主应用方法
async function callParentApp() {
  const mainApp = await bridge.getApp("main");
  const config = await mainApp.getConfig();
  console.log("主应用配置:", config);
}
```

## 多 Tab 场景（重点）

当同一个子应用需要打开多个 Tab 时，需要使用 `instanceId` 来区分不同实例。

### 主应用 - 创建 Tab 并注入 instanceId

```typescript
import { startApp } from "wujie";
import {
  ComlinkBridge,
  AppType,
  generateUUID,
  createInstancePlugin,
} from "sl-evolink";

// 创建 Bridge
const bridge = new ComlinkBridge({
  appName: "main",
  appType: AppType.SLY,
  api: {
    /* ... */
  },
});

// Tab 数据结构
interface Tab {
  id: string;
  instanceId: string; // 核心：与 AppManager 绑定的唯一标识
  appName: string; // 子应用名称（可重复）
  url: string;
}

// 创建新 Tab
async function createTab(appName: string, url: string) {
  // 1. 预先生成 instanceId
  const instanceId = generateUUID();

  // 2. 创建 Tab
  const tab: Tab = {
    id: `tab-${instanceId.slice(0, 8)}`,
    instanceId,
    appName,
    url,
  };

  // 3. 使用 createInstancePlugin 注入 instanceId
  await startApp({
    name: `wujie-${instanceId.slice(0, 8)}`,
    url,
    el: "#container",
    plugins: [
      createInstancePlugin({
        instanceId: tab.instanceId,
        tabId: tab.id,
        onWindowReady: (childWindow) => {
          console.log("子应用窗口已创建");
        },
      }),
    ],
  });

  return tab;
}

// 通过 instanceId 获取子应用（精确匹配）
async function getAppByTab(tab: Tab) {
  return bridge.getAppByInstanceId(tab.instanceId);
}

// 关闭 Tab 时注销
function closeTab(tab: Tab) {
  bridge.removeAppByInstanceId(tab.instanceId);
}
```

## 普通 iframe 支持

SDK 同时支持普通 iframe，适用于以下场景：

- 渐进式迁移：部分应用使用 wujie，部分使用普通 iframe
- 简单集成：不需要 wujie 的沙箱能力
- 跨团队协作：子应用团队使用不同技术栈

### 主应用 - 加载普通 iframe

**方式 1：使用 `loadIframe` 辅助函数（推荐）**

```typescript
import { loadIframe, generateUUID, ComlinkBridge, AppType } from "sl-evolink";

// 创建主应用 Bridge
const bridge = new ComlinkBridge({
  appName: "main",
  appType: AppType.SLY,
  api: {
    /* ... */
  },
});

// 加载子应用 iframe
const container = document.getElementById("app-container");
const { iframe, instanceId, destroy } = loadIframe({
  container, // DOM 元素
  url: "http://localhost:3001", // 子应用地址
  tabId: "tab-1", // 可选：关联的 Tab ID
  instanceId: generateUUID(), // 可选：不传则自动生成
  attrs: { allow: "fullscreen" }, // 可选：额外的 iframe 属性
  params: { theme: "dark" }, // 可选：额外的 URL 参数
  onLoad: (iframe, instanceId) => {
    console.log("子应用加载完成:", instanceId);
  },
});

// 保存 Tab 映射
tabMap.set("tab-1", { instanceId });

// 获取子应用代理
const childApp = await bridge.getAppByInstanceId(instanceId);
await childApp.someMethod();

// 销毁
destroy();
```

生成的 URL 格式：

```
http://localhost:3001/?__instanceId=550e8400-e29b-41d4-a716-446655440000&__tabId=tab-1&theme=dark
```

**方式 2：手动创建 iframe**

```typescript
import { buildUrlWithInstanceId, generateUUID } from "sl-evolink";

const instanceId = generateUUID();
const iframe = document.createElement("iframe");
iframe.src = buildUrlWithInstanceId("http://localhost:3001", instanceId);
// 生成: http://localhost:3001/?__instanceId=xxx

container.appendChild(iframe);
```

### 子应用 - 代码无需修改

子应用代码 **完全相同**，无论是 wujie 还是普通 iframe：

```typescript
import { ComlinkBridge, AppType } from "sl-evolink";

// SDK 自动检测 instanceId 来源：
// 1. options.instanceId（手动传入）
// 2. window.__COMLINK_PREASSIGNED_INSTANCE_ID__（wujie 注入）
// 3. URL 参数 ?__instanceId=xxx（普通 iframe）
// 4. 自动生成 UUID
const bridge = new ComlinkBridge({
  appName: "child-app",
  appType: AppType.CY,
  api: {
    async getStatus() {
      return { appName: "child-app", version: "1.0.0" };
    },
  },
});

await bridge.waitReady();
```

### wujie 和 iframe 对比

| 对比项          | wujie                            | 普通 iframe                  |
| --------------- | -------------------------------- | ---------------------------- |
| 通信机制        | `postMessage` + `MessageChannel` | 相同                         |
| instanceId 注入 | `createInstancePlugin`           | URL 参数 `?__instanceId=xxx` |
| 子应用代码      | `new ComlinkBridge({ ... })`     | 相同                         |
| JS 沙箱         | 有                               | 无                           |
| CSS 隔离        | 有                               | 天然隔离                     |
| 路由同步        | 支持                             | 需手动处理                   |

## API 参考

### ComlinkBridge

主要的通信桥接类。

```typescript
const bridge = new ComlinkBridge({
  appName: string,        // 应用名称
  appType: AppType,       // 应用类型
  instanceId?: string,    // 实例ID（可选，不传则自动生成或使用注入的）
  timeout?: number,       // 超时时间，默认 10000ms
  api?: object            // 暴露给其他应用的 API
})
```

**方法：**

| 方法                                | 说明                                 |
| ----------------------------------- | ------------------------------------ |
| `getApp<T>(appName)`                | 通过 appName 获取应用代理            |
| `getAppByInstanceId<T>(instanceId)` | 通过 instanceId 获取应用代理（推荐） |
| `getAppByType<T>(appType)`          | 通过应用类型获取代理                 |
| `getAppsByName<T>(appName)`         | 获取同名的所有实例                   |
| `removeApp(appName)`                | 移除应用（按 appName）               |
| `removeAppByInstanceId(instanceId)` | 移除应用（按 instanceId，推荐）      |
| `waitReady(timeout?)`               | 等待就绪                             |
| `getRegisteredApps()`               | 获取所有已注册应用名                 |
| `getRegisteredInstances()`          | 获取所有实例详情                     |
| `onAppRegistered(callback)`         | 监听应用注册事件                     |
| `onAppRemoved(callback)`            | 监听应用移除事件                     |

### AppType

应用类型枚举：

```typescript
enum AppType {
  CY = "CY", // 餐饮系统
  CRM = "CRM", // CRM 系统
  SLY = "SLY", // 商龙云
  AI = "AI", // AI 助手
  SCM = "SCM", // 供应链系统
}
```

### 工具函数

```typescript
import {
  // 通用
  generateUUID, // 生成 UUID
  generateShortId, // 生成短 ID（8位）

  // wujie 专用
  createInstancePlugin, // 创建 wujie plugin
  getPreassignedInstanceId, // 获取 wujie 注入的 instanceId
  getTabId, // 获取 wujie 注入的 Tab ID

  // 普通 iframe 专用
  loadIframe, // 加载 iframe 并注入 instanceId
  buildUrlWithInstanceId, // 构建带 instanceId 的 URL
  getInstanceIdFromUrl, // 从 URL 读取 instanceId
  getTabIdFromUrl, // 从 URL 读取 Tab ID
} from "sl-evolink";
```

### createInstancePlugin（wujie 专用）

创建 wujie plugin，用于注入 `instanceId` 到子应用：

```typescript
createInstancePlugin({
  instanceId: string,              // 必填，实例ID
  tabId?: string,                  // 可选，Tab ID
  onWindowReady?: (win) => void    // 可选，窗口就绪回调
})
```

### loadIframe（普通 iframe 专用）

加载普通 iframe 并自动注入 `instanceId`：

```typescript
interface IframeLoaderOptions {
  container: HTMLElement           // iframe 容器元素
  url: string                      // 子应用 URL
  instanceId?: string              // 可选，不传则自动生成
  tabId?: string                   // 可选，Tab ID
  name?: string                    // 可选，iframe name 属性
  attrs?: Record<string, string>   // 可选，额外的 iframe 属性
  params?: Record<string, string>  // 可选，额外的 URL 参数
  onLoad?: (iframe, instanceId) => void   // 加载完成回调
  onError?: (error) => void        // 加载失败回调
}

interface IframeLoaderResult {
  iframe: HTMLIFrameElement        // iframe 元素
  instanceId: string               // 分配的 instanceId
  destroy: () => void              // 销毁方法
}

const result = loadIframe(options): IframeLoaderResult
```

## 高级用法

### 暴露 Class 给远程调用

```typescript
// 子应用
import { ComlinkBridge, AppType, proxy } from "sl-evolink";

class Calculator {
  private value = 0;

  add(n: number) {
    this.value += n;
    return this.value;
  }

  getValue() {
    return this.value;
  }
}

const bridge = new ComlinkBridge({
  appName: "calc-app",
  appType: AppType.SLY,
  api: {
    // 方式1：直接暴露 Class
    Calculator,

    // 方式2：返回实例（需要 proxy 包装）
    createCalculator(initial: number) {
      return proxy(new Calculator(initial));
    },
  },
});
```

```typescript
// 主应用调用
const app = await bridge.getApp("calc-app");

// 远程实例化
const calc = await new app.Calculator(100);
await calc.add(50);
const value = await calc.getValue(); // 150

// 或使用工厂方法
const calc2 = await app.createCalculator(200);
await calc2.add(100);
```

### 返回复杂对象

如果返回值包含函数，需要使用 `proxy` 包装：

```typescript
import { proxy } from "sl-evolink";

const bridge = new ComlinkBridge({
  appName: "my-app",
  appType: AppType.SLY,
  api: {
    async getData() {
      return proxy({
        value: 100,
        // 返回的对象包含函数
        format: (prefix: string) => `${prefix}: ${100}`,
      });
    },
  },
});
```

### 子应用间直连

```typescript
// 子应用 A 调用子应用 B
const appB = await bridge.getApp("app-b");
const result = await appB.someMethod();
```

### 监听应用注册/移除事件

当有新的子应用注册或现有应用被移除时，可以通过事件监听来实时响应：

```typescript
import { ComlinkBridge, AppType } from "sl-evolink";
import type { AppManagerEventData } from "sl-evolink";

const bridge = new ComlinkBridge({
  appName: "my-app",
  appType: AppType.CRM,
  api: {
    /* ... */
  },
});

// 监听新应用注册
bridge.onAppRegistered((info: AppManagerEventData) => {
  // 过滤掉自己
  if (info.instanceId === bridge.instanceId) return;

  console.log(`新应用加入: ${info.appName}`);
  console.log(`实例ID: ${info.instanceId}`);
  console.log(`应用类型: ${info.appType}`);

  // 刷新应用列表
  refreshPeerApps();
});

// 监听应用移除
bridge.onAppRemoved((info: AppManagerEventData) => {
  // 过滤掉自己
  if (info.instanceId === bridge.instanceId) return;

  console.log(`应用离开: ${info.appName}`);

  // 清理相关状态
  if (selectedPeer?.instanceId === info.instanceId) {
    selectedPeer = null;
  }

  refreshPeerApps();
});
```

**AppManagerEventData 类型：**

```typescript
interface AppManagerEventData {
  instanceId: string; // 实例唯一标识
  appName: string; // 应用名称
  appType: AppType; // 应用类型
  timestamp: number; // 事件时间戳
}
```

**使用场景：**

- 实时更新已连接应用列表
- 当目标应用离线时，自动清理相关状态
- 显示应用上下线通知

## 完整示例

参考 `examples/` 目录：

- `examples/main/` - 主应用示例（Vue 3）
- `examples/sub-vue2/` - Vue 2 子应用示例
- `examples/sub-vue3/` - Vue 3 子应用示例

## License

MIT