Window トランスポート
このモードを使うとき
見出しへのリンクwindow.open トランスポートは、リモートランタイムが独自の可視ワークスペースを必要とする場合に適しています:
- 拡張がツールパネルやエディタのように振る舞う。
- リモート UI をホストのレイアウトやライフサイクルから分離したい。
- iframe 埋め込みなしで明示的な postMessage 境界を持ちたい。
基本モデル
見出しへのリンクホストがポップアップウィンドウを開き、双方が 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() },})運用上の注意点
見出しへのリンク- ポップアップブロック: ユーザーの直接操作からポップアップを開く。
- Origin チェック:
event.originとevent.sourceを必ず検証する。 - ライフサイクル同期: ポップアップの終了とホストのアンロードを対称に扱う。
- セキュリティモデル: これはトランスポート境界であり、完全なポリシーガバナンスではない。