遊戲大廳裡 14 款遊戲都是用 iframe 嵌入 play.html。每款遊戲在自己的 iframe 沙箱裡跑,跟外殼大廳之間透過一套自製 SDK 通訊:上傳分數、讀取使用者、觸發廣告、回報遊玩進度。這套通訊機制看似簡單,但牽涉跨來源安全、postMessage 協定設計、版本管理三個複雜議題。
為什麼用 iframe 而不是直接 import
三個優勢:
- 故障隔離:一款遊戲爆掉不會帶倒整個大廳
- 狀態隔離:每款遊戲的 localStorage 命名空間獨立
- 外部嵌入:遊戲檔案可以獨立發布到 itch.io、Gumroad,不依賴大廳
SDK 的雙向設計
iframe 跟父頁面(大廳)之間有兩個通訊方向:
| 方向 | 用途 |
|---|---|
| 遊戲 → 大廳 | 上傳分數、回報玩遊戲、請求廣告播放、登出 |
| 大廳 → 遊戲 | 傳遞使用者資訊、暫停 / 繼續、調整音量 |
同源 vs 跨源
本站架構:
- 大廳:
https://game.hao0321.com/play.html - 遊戲:
https://game.hao0321.com/dodge-master-v5.html
同源(同 protocol、同 domain、同 port)所以可以用 window.parent 直接互讀。但為了未來想嵌入別站,還是設計了 postMessage 協定。
SDK 的 API 設計
大廳暴露給遊戲的全域物件:
// 遊戲端使用方式
window.parent.haoGame.reportScore(8240);
window.parent.haoGame.getUser(); // → { id, name, coins, ... }
window.parent.haoGame.requestAd('revive', { reward: 50 });
window.parent.haoGame.setReady();
// 大廳端定義 (在 play.html)
window.haoGame = {
reportScore(score) { /* 上傳到 api.hao0321.com */ },
getUser() { return JSON.parse(localStorage.hao_user || 'null'); },
requestAd(type, params) { /* 觸發 AdSense */ },
setReady() { /* 隱藏 loading 動畫 */ },
};
跨源備援:postMessage 協定
當遊戲被嵌到其他網域時,window.parent 會被瀏覽器擋掉。SDK 自動降級到 postMessage:
// 遊戲端 SDK
function reportScore(score) {
if (sameOrigin()) {
window.parent.haoGame.reportScore(score);
} else {
window.parent.postMessage({
type: 'reportScore',
payload: { score },
__hao: true,
}, '*');
}
}
function sameOrigin() {
try {
return window.parent.location.origin === window.location.origin;
} catch {
return false;
}
}
大廳端 listener:
window.addEventListener('message', (e) => {
// 安全檢查
if (!e.data || !e.data.__hao) return;
if (!isAllowedOrigin(e.origin)) return;
switch (e.data.type) {
case 'reportScore':
reportScore(e.data.payload.score);
break;
case 'requestAd':
requestAd(e.data.payload.type);
break;
}
});
安全檢查:origin allowlist
postMessage 最常見的漏洞是接受任何來源訊息。修法:
const ALLOWED_ORIGINS = [
'https://game.hao0321.com',
'https://hao0321.com',
'https://itch.io',
'https://gum.co',
];
function isAllowedOrigin(origin) {
return ALLOWED_ORIGINS.some(o =>
origin === o || origin.endsWith('.' + o.replace(/^https?:\/\//, ''))
);
}
版本管理:API 演化
SDK 一旦發布到外部,就要永遠相容。我用 __hao_version 欄位標記:
window.parent.postMessage({
__hao: true,
__hao_version: 2,
type: 'reportScore',
payload: { score: 8240, gameId: 'dodge-master' },
}, '*');
大廳端根據版本決定怎麼處理。新版可以加欄位但不能改舊欄位語意。
登入狀態同步
玩家在大廳登入後,狀態存在 localStorage。但 iframe 因為同源所以可以共享 — 不需要額外傳遞。
跨源情境(嵌到別站)就需要明確傳:
// 遊戲端
function getUser() {
return new Promise((resolve) => {
if (sameOrigin()) {
resolve(JSON.parse(localStorage.hao_user || 'null'));
return;
}
const handler = (e) => {
if (e.data.__hao_reply === 'getUser') {
window.removeEventListener('message', handler);
resolve(e.data.payload);
}
};
window.addEventListener('message', handler);
window.parent.postMessage({ __hao: true, type: 'getUser' }, '*');
setTimeout(() => resolve(null), 1000); // timeout
});
}
X-Frame-Options 踩坑
Cloudflare Pages 預設會把 X-Frame-Options: DENY 加到所有檔案,連同源 iframe 都被擋。修法:在 _headers 改成 SAMEORIGIN(或對 game/* 路徑單獨設):
/* X-Frame-Options: SAMEORIGIN /game/* X-Frame-Options: DENY # 遊戲檔案禁止其他站嵌入
除錯技巧
iframe 通訊壞掉時很難 debug,因為 console log 在不同 frame。我加了一個 wrapper:
function log(...args) {
console.log('[GAME]', ...args);
if (sameOrigin()) {
window.parent.console.log('[FROM GAME]', ...args);
}
}
這樣大廳的 console 也看得到遊戲的 log,方便整合除錯。
結語
iframe + postMessage 是被低估的 web 平台特性。用對了可以做出組件化、可獨立發布、安全隔離的應用架構。本站 14 款遊戲共用一套 SDK,新遊戲開發只需要呼叫 5 個 API 就能整合到大廳。
SDK 完整原始碼(約 200 行)我計畫整理成 GitHub repo 開放。有興趣可以寄信 lo246179268@gmail.com。