Iframe 統合
トランスポート非依存のアーキテクチャから見たい場合は 概要 から始めてください。
なぜ iframe モードなのか
見出しへのリンクiframe トランスポートは、分離されたリモート拡張向けのブラウザ基準構成です:
- ホストとリモートが別々のブラウザコンテキストで動作する。
- 通信には
@remote-ui/rpcを介したpostMessageを使う。 - ホストは provider と receiver の境界を通じて描画制御を維持する。
ホスト側の例
見出しへのリンクimport type { Channel } from '@omnicajs/vue-remote/host'import type { Endpoint } from '@remote-ui/rpc'
import { createApp, defineComponent, h, onBeforeUnmount, onMounted, ref } from 'vue'import { HostedTree, createProvider, createReceiver } from '@omnicajs/vue-remote/host'import { createEndpoint, fromIframe } from '@remote-ui/rpc'
import VButton from './components/VButton.vue'import VInput from './components/VInput.vue'
type HostBridge = { track(event: { type: string; payload: Record<string, unknown> }): void;}
type RemoteApi = { run(channel: Channel, bridge: HostBridge): Promise<void>; release(): void;}
const provider = createProvider({ VButton, VInput })
export function mountIframeRemote(remoteUrl: string, bridge: HostBridge) { return createApp(defineComponent({ setup() { const iframe = ref<HTMLIFrameElement | null>(null) const receiver = createReceiver() let endpoint: Endpoint<RemoteApi> | null = null
onMounted(() => { endpoint = createEndpoint<RemoteApi>(fromIframe(iframe.value as HTMLIFrameElement, { terminate: false, })) })
onBeforeUnmount(() => endpoint?.call.release())
return () => [ h(HostedTree, { provider, receiver }), h('iframe', { ref: iframe, src: remoteUrl, style: { display: 'none' }, onLoad: () => endpoint?.call.run(receiver.receive, bridge), }), ] }, }))}リモート側の例
見出しへのリンクimport { createEndpoint, fromInsideIframe, release, retain } from '@remote-ui/rpc'import { createRemoteRenderer, createRemoteRoot, defineRemoteComponent } from '@omnicajs/vue-remote/remote'import { defineComponent, h, ref } from 'vue'
type HostBridge = { track(event: { type: string; payload: Record<string, unknown> }): void;}
const VButton = defineRemoteComponent('VButton')const VInput = defineRemoteComponent('VInput', [ 'update:value',] as unknown as { 'update:value': (value: string) => true;})
const endpoint = createEndpoint(fromInsideIframe())let onRelease = () => {}
endpoint.expose({ async run(channel, bridge: HostBridge) { retain(channel) retain(bridge)
const root = createRemoteRoot(channel, { components: ['VButton', 'VInput'], }) await root.mount()
const app = createRemoteRenderer(root).createApp(defineComponent({ setup() { const text = ref('') return () => [ h(VInput, { value: text.value, 'onUpdate:value': (value: string) => text.value = value, }), h(VButton, { onClick: () => bridge.track({ type: 'remote.clear', payload: { value: text.value }, }), }, 'Clear'), ] }, }))
app.mount(root)
onRelease = () => { release(channel) release(bridge) app.unmount() } },
release() { onRelease() },})Iframe の起動シーケンス
見出しへのリンク- ホストが
HostedTreeを描画し、receiverを作成する。 - ホストがリモート URL を隠し iframe に読み込む。
- リモート iframe が
run/releaseを公開する。 - iframe の
load時に、ホストがrun(receiver.receive, hostBridge)を呼ぶ。 - リモートがマウントし、チャネル経由でツリー更新を同期する。
- teardown 時にホストが
release()を呼ぶ。
Iframe セキュリティチェックリスト
見出しへのリンク- Origin を制限する: 許容しすぎるワイルドカードより、明示的な target origin を優先する。
- リモート URL を検証する: 拡張ランタイムの配信元には allowlist を使う。
- iframe 属性を強化する:
信頼モデルに合わせて
sandbox能力を構成する。 - CSP / frame ポリシーを適用する:
frame-src/child-srcを承認済みの拡張 origin と一致させる。
よくある落とし穴
見出しへのリンク- iframe の読み込み前に
run(...)を呼んでしまう。 - provider にホストコンポーネントを登録し忘れる。
- 境界をまたいで非シリアライズ可能な payload を渡す。
release()を忘れて retained 参照をリークさせる。