メインコンテンツへ移動

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()
},
})

運用上の注意点

見出しへのリンク
  1. ポップアップブロック: ユーザーの直接操作からポップアップを開く。
  2. Origin チェック: event.originevent.source を必ず検証する。
  3. ライフサイクル同期: ポップアップの終了とホストのアンロードを対称に扱う。
  4. セキュリティモデル: これはトランスポート境界であり、完全なポリシーガバナンスではない。

関連ドキュメント

見出しへのリンク