Window 传输
什么时候使用这种模式
Section titled “什么时候使用这种模式”当远程运行时需要独立可见的工作空间时,window.open 传输很适合:
- 扩展表现为工具面板或编辑器;
- 远程 UI 需要与宿主布局和生命周期隔离;
- 你希望拥有显式的 postMessage 边界,而不是嵌入 iframe。
宿主打开一个弹出窗口,双方通过 postMessage 交换消息。
@remote-ui/rpc 可以通过自定义 MessageEndpoint 包装器在这里工作。
Host App -> popup = window.open(...) -> createEndpoint(fromWindow(popup)) -> call.run(receiver.receive, hostBridge)
Popup Remote App -> createEndpoint(fromOpener(window.opener)) -> expose run/releaseimport type { MessageEndpoint } from '@remote-ui/rpc'import type { Channel } from '@omnicajs/vue-remote/host'
import { createEndpoint } from '@remote-ui/rpc'import { createReceiver } from '@omnicajs/vue-remote/host'
type RemoteApi = { run(channel: Channel, bridge: { closeRequested(): void }): Promise<void>; release(): void;}
function fromWindow(target: Window, targetOrigin: string): MessageEndpoint { const listeners = new Set<(event: MessageEvent) => void>()
const onMessage = (event: MessageEvent) => { if (event.origin !== targetOrigin) return if (event.source !== target) return for (const listener of listeners) listener(event) }
window.addEventListener('message', onMessage)
return { postMessage(message: any) { target.postMessage(message, targetOrigin) }, addEventListener(event, listener) { if (event === 'message') listeners.add(listener) }, removeEventListener(event, listener) { if (event === 'message') listeners.delete(listener) }, terminate() { window.removeEventListener('message', onMessage) target.close() }, }}
const popup = window.open('/remote/popup.html', 'remote-runtime', 'width=1200,height=800')if (!popup) throw new Error('Popup blocked')
const receiver = createReceiver()const endpoint = createEndpoint<RemoteApi>(fromWindow(popup, window.location.origin))
await endpoint.call.run(receiver.receive, { closeRequested() { popup.close() },})远程弹窗侧示例
Section titled “远程弹窗侧示例”import type { MessageEndpoint } from '@remote-ui/rpc'import type { Channel } from '@omnicajs/vue-remote/host'
import { createEndpoint, release, retain } from '@remote-ui/rpc'import { createRemoteRoot, createRemoteRenderer, defineRemoteComponent } from '@omnicajs/vue-remote/remote'import { defineComponent, h } from 'vue'
function fromOpener(opener: Window, targetOrigin: string): MessageEndpoint { const listeners = new Set<(event: MessageEvent) => void>()
const onMessage = (event: MessageEvent) => { if (event.origin !== targetOrigin) return if (event.source !== opener) return for (const listener of listeners) listener(event) }
window.addEventListener('message', onMessage)
return { postMessage(message: any) { opener.postMessage(message, targetOrigin) }, addEventListener(event, listener) { if (event === 'message') listeners.add(listener) }, removeEventListener(event, listener) { if (event === 'message') listeners.delete(listener) }, terminate() { window.removeEventListener('message', onMessage) window.close() }, }}
const endpoint = createEndpoint<{ run(channel: Channel, bridge: { closeRequested(): void }): Promise<void>; release(): void;}>(fromOpener(window.opener as Window, window.location.origin))
let onRelease = () => {}
const VInspectorBadge = defineRemoteComponent('VInspectorBadge')
endpoint.expose({ async run(channel, bridge) { retain(channel) retain(bridge)
const root = createRemoteRoot(channel, { components: ['VInspectorBadge'] }) await root.mount()
const app = createRemoteRenderer(root).createApp(defineComponent({ setup() { return () => h(VInspectorBadge, { tone: 'neutral', label: 'Popup runtime connected', onClose: () => bridge.closeRequested(), }) }, }))
app.mount(root)
onRelease = () => { release(channel) release(bridge) app.unmount() } }, release() { onRelease() },})运行注意事项
Section titled “运行注意事项”- 弹窗拦截: 必须从用户的直接操作中打开弹窗。
- Origin 校验:
始终校验
event.origin和event.source。 - 生命周期同步: 对称处理弹窗关闭与宿主卸载。
- 安全模型: 这是一条传输边界,不是完整的策略治理层。