@sl-material/sl-evolink
v1.0.0
Published
商龙通信进化 SDK - 微前端/iframe 应用间通信解决方案
Maintainers
Readme
sl-evolink
商龙通信进化 SDK - 微前端/iframe 应用间通信解决方案
基于 Comlink 封装,提供类型安全的 RPC 调用,支持 wujie、普通 iframe 等微前端场景。
特性
- 类型安全的远程方法调用
- 支持多实例管理(多 Tab 场景)
- 支持 Class 远程实例化
- 支持子应用间直连通信
- 同时兼容 wujie 和普通 iframe(渐进式迁移友好)
- 兼容 wujie、qiankun、iframe 等微前端方案
安装
npm install sl-evolink
# 或
pnpm add sl-evolink快速开始
1. 主应用(父页面)
// 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. 子应用(子页面)
// 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
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 辅助函数(推荐)
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
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:
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
主要的通信桥接类。
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
应用类型枚举:
enum AppType {
CY = "CY", // 餐饮系统
CRM = "CRM", // CRM 系统
SLY = "SLY", // 商龙云
AI = "AI", // AI 助手
SCM = "SCM", // 供应链系统
}工具函数
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 到子应用:
createInstancePlugin({
instanceId: string, // 必填,实例ID
tabId?: string, // 可选,Tab ID
onWindowReady?: (win) => void // 可选,窗口就绪回调
})loadIframe(普通 iframe 专用)
加载普通 iframe 并自动注入 instanceId:
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 给远程调用
// 子应用
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));
},
},
});// 主应用调用
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 包装:
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}`,
});
},
},
});子应用间直连
// 子应用 A 调用子应用 B
const appB = await bridge.getApp("app-b");
const result = await appB.someMethod();监听应用注册/移除事件
当有新的子应用注册或现有应用被移除时,可以通过事件监听来实时响应:
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 类型:
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
