feat(iframe-bridge): implement iframe communication bridge and language sync
This commit is contained in:
@@ -1,149 +1,31 @@
|
||||
// 入口文件:仅聚合与导出 API,不承载具体逻辑
|
||||
export type { HostApi, ChildApi } from './types';
|
||||
export { isEmbedded, toHostPath, toChildPath } from './path';
|
||||
export {
|
||||
getClientHostApi,
|
||||
clientNavigate,
|
||||
clientClose,
|
||||
clientReady,
|
||||
initChildBridge,
|
||||
} from './client';
|
||||
export { createPenpalHostBridge } from './host';
|
||||
|
||||
// 默认导出一个聚合对象:
|
||||
// - 兼容某些只识别 default 的打包方案或联邦场景
|
||||
// - 同时保留命名导出,建议优先使用命名导出
|
||||
import { isEmbedded, toHostPath, toChildPath } from './path';
|
||||
import {
|
||||
getClientHostApi,
|
||||
clientNavigate,
|
||||
clientClose,
|
||||
clientReady,
|
||||
initChildBridge,
|
||||
} from './client';
|
||||
import { createPenpalHostBridge } from './host';
|
||||
|
||||
/**
|
||||
* @teres/iframe-bridge
|
||||
* Penpal-based utilities for host ↔ iframe communication,
|
||||
* plus minimal helpers for embed detection and path conversion.
|
||||
* 默认聚合导出对象:包含全部对外 API。
|
||||
*/
|
||||
import { connectToChild, connectToParent } from 'penpal';
|
||||
|
||||
// Penpal RPC API definitions
|
||||
export interface HostApi {
|
||||
navigate: (to: string) => void;
|
||||
close: () => void;
|
||||
agentReady?: (payload?: any) => void;
|
||||
}
|
||||
|
||||
export interface ChildApi {
|
||||
navigate?: (to: string) => void;
|
||||
close?: () => void;
|
||||
ready?: (payload?: any) => void;
|
||||
}
|
||||
|
||||
// Unified logger for debugging host/child interactions
|
||||
const LOG_PREFIX = '[@teres/iframe-bridge]';
|
||||
function log(...args: any[]) {
|
||||
try {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(LOG_PREFIX, ...args);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function safePathname(): string {
|
||||
try {
|
||||
const p = window.location?.pathname || '';
|
||||
log('safePathname()', p);
|
||||
return p;
|
||||
} catch {
|
||||
log('safePathname() error, return empty pathname');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isInIframe(): boolean {
|
||||
try {
|
||||
const inIframe = window.self !== window.top;
|
||||
log('isInIframe()', inIframe);
|
||||
return inIframe;
|
||||
} catch {
|
||||
// Cross-origin access throws, treat as embedded
|
||||
log('isInIframe() cross-origin error, treat as embedded');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function isEmbedded(): boolean {
|
||||
const path = safePathname();
|
||||
const inIframe = isInIframe();
|
||||
const embedded = path.includes('/route-ragflow') || inIframe;
|
||||
log('isEmbedded()', { path, inIframe, embedded });
|
||||
return embedded;
|
||||
}
|
||||
|
||||
/** Add "/ragflow" prefix when targeting host routes. */
|
||||
export function toHostPath(to: string): string {
|
||||
const normalized = to.startsWith('/') ? to : `/${to}`;
|
||||
const target = normalized.startsWith('/route-ragflow') ? normalized : `/ragflow${normalized}`;
|
||||
log('toHostPath()', { input: to, normalized, target });
|
||||
return target;
|
||||
}
|
||||
|
||||
/** Remove leading "/route-ragflow" prefix when targeting child routes. */
|
||||
export function toChildPath(to: string): string {
|
||||
const target = to.replace(/^\/route-ragflow/, '') || '/';
|
||||
log('toChildPath()', { input: to, target });
|
||||
return target;
|
||||
}
|
||||
|
||||
// Cached Penpal connections
|
||||
let clientHostApiPromise: Promise<HostApi> | null = null;
|
||||
|
||||
/** Create a Penpal connection from child to parent and cache the Host API. */
|
||||
export function getClientHostApi(options?: { parentOrigin?: string }): Promise<HostApi> {
|
||||
if (!isEmbedded()) {
|
||||
log('getClientHostApi() aborted: not embedded');
|
||||
return Promise.reject(new Error('Not embedded'));
|
||||
}
|
||||
if (!clientHostApiPromise) {
|
||||
const parentOrigin = options?.parentOrigin ?? window.location.origin;
|
||||
log('getClientHostApi() connecting to parent', { parentOrigin });
|
||||
clientHostApiPromise = connectToParent<HostApi>({ parentOrigin }).promise.then((host) => {
|
||||
log('getClientHostApi() connected', {
|
||||
hasNavigate: typeof host.navigate === 'function',
|
||||
hasClose: typeof host.close === 'function',
|
||||
});
|
||||
return host;
|
||||
});
|
||||
}
|
||||
return clientHostApiPromise;
|
||||
}
|
||||
|
||||
export async function clientNavigate(to: string): Promise<void> {
|
||||
log('clientNavigate() begin', { to });
|
||||
const host = await getClientHostApi();
|
||||
await host.navigate(to);
|
||||
log('clientNavigate() done', { to });
|
||||
}
|
||||
|
||||
export async function clientClose(): Promise<void> {
|
||||
log('clientClose() begin');
|
||||
const host = await getClientHostApi();
|
||||
log('clientClose() host api', {
|
||||
hasClose: typeof host.close === 'function',
|
||||
});
|
||||
await host.close();
|
||||
log('clientClose() done');
|
||||
}
|
||||
|
||||
export async function clientReady(payload?: any): Promise<void> {
|
||||
log('clientReady() begin', { payload });
|
||||
const host = await getClientHostApi();
|
||||
const agentReady = host.agentReady;
|
||||
const hasAgentReady = typeof agentReady === 'function';
|
||||
log('clientReady() host agentReady', { hasAgentReady });
|
||||
if (hasAgentReady) await agentReady!(payload);
|
||||
log('clientReady() done');
|
||||
}
|
||||
|
||||
/** Establish Penpal host bridge bound to an iframe and expose Host API to child. */
|
||||
export function createPenpalHostBridge(params: {
|
||||
iframe: HTMLIFrameElement;
|
||||
origin?: string;
|
||||
methods: HostApi;
|
||||
}) {
|
||||
const childOrigin = params.origin ?? window.location.origin;
|
||||
log('createPenpalHostBridge() start', { childOrigin });
|
||||
const connection = connectToChild<ChildApi>({ iframe: params.iframe, childOrigin, methods: params.methods as any });
|
||||
connection.promise.then(() => log('createPenpalHostBridge() child connected')).catch((err) => log('createPenpalHostBridge() connect error', err));
|
||||
return {
|
||||
child: connection.promise,
|
||||
destroy: () => {
|
||||
log('createPenpalHostBridge() destroy');
|
||||
return connection.destroy?.();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Provide a default export with all APIs to maximize compatibility with bundlers
|
||||
// that prefer default-only federation or mis-handle named ESM exports in dev.
|
||||
const bridge = {
|
||||
isEmbedded,
|
||||
toHostPath,
|
||||
@@ -153,6 +35,7 @@ const bridge = {
|
||||
clientClose,
|
||||
clientReady,
|
||||
createPenpalHostBridge,
|
||||
initChildBridge,
|
||||
};
|
||||
|
||||
export default bridge;
|
||||
|
||||
Reference in New Issue
Block a user