跳转到内容

Window 传输

当远程运行时需要独立可见的工作空间时,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/release
import 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()
},
})
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()
},
})
  1. 弹窗拦截: 必须从用户的直接操作中打开弹窗。
  2. Origin 校验: 始终校验 event.originevent.source
  3. 生命周期同步: 对称处理弹窗关闭与宿主卸载。
  4. 安全模型: 这是一条传输边界,不是完整的策略治理层。