From b8d1a0da734a97db23e415cfaaf0b1eb28824e53 Mon Sep 17 00:00:00 2001 From: zizi Date: Thu, 11 Jun 2026 02:45:31 +0000 Subject: [PATCH] =?UTF-8?q?fix(runtime-host):=20host=E2=86=92game=20?= =?UTF-8?q?=E5=9B=9E=E5=8C=85=E5=85=A8=E6=96=AD=E6=A0=B9=E5=9B=A0=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E2=80=94=E2=80=94v-for=20=E6=A8=A1=E6=9D=BF=20ref=20?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E8=A7=A3=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(探针四锚点+判别实验实证):ref=iframeRef 位于 v-for 内,Vue 3 填充为数组; attachBridge 按元素直取 (数组).contentWindow=undefined → HostBridge.targetWindow= undefined → post() 静默丢弃全部 host→game(storage/ad/pay 回包+init 同断)。 game→host 监听全局 window 不受影响,故游戏可玩/遥测可达,两波未现形。 修法:currentIframe() 解包 + contentWindow 不可用时显式 console.warn(可观测)。 Co-Authored-By: Claude Fable 5 --- game-studio/src/host/GamePlayer.vue | 31 ++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/game-studio/src/host/GamePlayer.vue b/game-studio/src/host/GamePlayer.vue index 6621af94..3d39e07e 100644 --- a/game-studio/src/host/GamePlayer.vue +++ b/game-studio/src/host/GamePlayer.vue @@ -55,10 +55,24 @@ type LoadPhase = 'idle' | 'loading' | 'ready' | 'error'; const phase = ref('idle'); const errorMsg = ref(''); -/** iframe 元素引用与 srcdoc 内容 */ -const iframeRef = ref(null); +/** + * iframe 元素引用与 srcdoc 内容。 + * ⚠ 模板里 ref="iframeRef" 位于 v-for="slot in containers" 内部——Vue 3 对 v-for 内的 + * 模板 ref 填充的是【数组】而非元素(即使只渲染 1 个 iframe)。曾因按元素直接取 + * .contentWindow 得 undefined,致 HostBridge.targetWindow=undefined、post() 静默丢弃 + * 全部 host→game 消息(storage/ad/pay 回包与 init 全断;game→host 不受影响故两波未现形)。 + * 取元素一律走 currentIframe() 解包,勿直接读 iframeRef.value。 + */ +const iframeRef = ref(null); const srcdoc = ref(''); +/** 解包 v-for 数组形态的模板 ref,返回当前唯一 iframe 元素(无则 null)。 */ +function currentIframe(): HTMLIFrameElement | null { + const v = iframeRef.value; + if (Array.isArray(v)) return v[0] ?? null; + return v; +} + /** 宿主桥实例(onMounted 后创建,卸载时 dispose) */ let bridge: HostBridge | null = null; @@ -200,16 +214,23 @@ let pendingAllowOrigins: string[] | undefined; /** 在 iframe 渲染后建立桥(含双校验) */ function attachBridge(): void { - const iframe = iframeRef.value; + const iframe = currentIframe(); // v-for 模板 ref 是数组,必须解包(见 iframeRef 声明注释) if (!iframe) return; // 先销毁旧桥 bridge?.dispose(); - // origin 白名单:srcdoc 注入文档 origin='null',需 includeNull=true 才能收到 iframe 消息。 + // origin 白名单:srcdoc 沙箱按 sandbox 形态 origin 可能为 'null'(无 allow-same-origin) + // 或与宿主同源(现行 allow-same-origin),两种均入白名单(includeNull=true + 自身 origin)。 const allowed = buildOriginAllowlist(pendingAllowOrigins, /* includeNull */ true); + const win = iframe.contentWindow; + if (!win) { + // host→game 出口不可用是致命静默故障(storage/ad/pay 回包全断),必须可观测 + // eslint-disable-next-line no-console + console.warn('[GamePlayer] attachBridge:iframe.contentWindow 不可用,host→game 方向将不可达'); + } bridge = new HostBridge({ - targetWindow: iframe.contentWindow, + targetWindow: win, allowedOrigins: allowed, onMessage: handleEnvelope, onReject: handleReject,