Fullscreen API 與 Screen Orientation API 是兩個「桌機與 Android 上很好用、iPhone 上不能依賴」的能力。caniuse 對 Fullscreen API 的標記是:Safari macOS 自 16.4 起完整支援,Safari iOS 從 12 到最新版都只是 partial。Screen Orientation 的 lock() 在 MDN 上更直接標為 Limited availability。這代表任何用到它們的程式碼都必須先 feature-detect,並準備一條完全不依賴它們的降級路徑。
Fullscreen API 的正確用法
async function enterFullscreen(el) {
// 必須在使用者手勢的呼叫堆疊中(click / pointerup / keydown),
// 不能放在 setTimeout 或 await 之後
if (!document.fullscreenEnabled) return false
try {
await el.requestFullscreen({ navigationUI: 'hide' })
return true
} catch (err) {
console.warn('fullscreen rejected:', err)
return false
}
}
document.addEventListener('fullscreenchange', () => {
const on = !!document.fullscreenElement
root.classList.toggle('is-fullscreen', on)
// 尺寸變化由 ResizeObserver 處理,這裡不要自己量
})幾個規範層面的重點:
- 需要 transient user activation。 使用者必須剛剛互動過。常見錯誤是
await someAsyncThing()之後才呼叫requestFullscreen()——activation 已經過期,Promise 直接 reject。 navigationUI: 'hide'要求瀏覽器把導覽列完全讓出來;'show'則保留導覽列並縮小元素;'auto'交給瀏覽器決定。這是請求不是保證。keyboardLock: 'browser'可以把 Esc、Ctrl+W 等按鍵導向頁面(需要preventDefault()攔截),對全螢幕遊戲的按鍵配置很有用,但支援度有限,且部分瀏覽器需要額外的使用者同意。- iframe 需要權限。 嵌在別人網站的遊戲,父層必須寫
allow="fullscreen",否則document.fullscreenEnabled為false。 - 失敗原因要分辨。
TypeError表示文件未啟用、元素不在文件中、或被 Permissions Policy 擋掉;不是所有失敗都值得重試。
必須提供退出入口。 全螢幕狀態下瀏覽器的返回鍵通常不可見,遊戲內一定要有明顯的離開按鈕,並同時監聽 Esc(瀏覽器會自動退出,但你的狀態要跟上 fullscreenchange)。
鎖定方向
async function lockLandscape() {
if (!screen.orientation?.lock) return false
try {
await screen.orientation.lock('landscape')
return true
} catch (err) {
// NotSupportedError(桌機/iOS)、SecurityError(未全螢幕)、AbortError(另一個 lock 進行中)
return false
}
}MDN 的說明很明確:方向鎖定通常只在行動裝置、且瀏覽器處於全螢幕狀態時可用。所以正確的順序是「先 requestFullscreen(),成功後才 lock()」,而且兩者都可能失敗。
可鎖定的值:any、natural、portrait、landscape、portrait-primary、portrait-secondary、landscape-primary、landscape-secondary。一般用 'landscape' 或 'portrait' 即可,鎖到 -primary 會讓使用者無法把手機轉 180 度(左手持機習慣)。
Safari 完全不支援 screen.orientation.lock()。 iPhone 上要強制橫向,只剩兩條路:CSS 軟性旋轉(見 方向與裝置 的策略 C),或顯示「請旋轉裝置」提示。後者體驗較差但實作單純、不會壞掉,多數產品選它。
iPhone 上真正可行的沉浸方案:PWA standalone
加到主畫面後以 standalone 模式啟動,是 iPhone 上唯一穩定拿到「無瀏覽器工具列的整塊螢幕」的方法。
manifest.json:
{
"name": "遊戲名稱",
"short_name": "遊戲",
"display": "fullscreen",
"display_override": ["fullscreen", "standalone"],
"orientation": "landscape",
"background_color": "#000000",
"theme_color": "#000000",
"start_url": "/?source=pwa"
}搭配 HTML 端的 Apple 專用 meta(見 視口設定):
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">要注意的落差:
manifest的orientation欄位 iOS 不理會,只有 Android 的 standalone PWA 會遵守。display: fullscreen在 iOS 上被當作standalone處理——狀態列仍在,但工具列消失,這已經是很大的改善。black-translucent讓內容延伸到狀態列底下,此時env(safe-area-inset-top)就是你避開狀態列的唯一依據。- 偵測是否處於 standalone:
matchMedia('(display-mode: standalone)').matches,iOS 舊版另有navigator.standalone。
const isStandalone =
matchMedia('(display-mode: standalone)').matches ||
matchMedia('(display-mode: fullscreen)').matches ||
navigator.standalone === true防止系統手勢與選單干擾
沉浸模式下最惱人的是系統或瀏覽器的預設行為插進來:
.game-root {
touch-action: none; /* 不捲動、不縮放、不雙擊放大 */
-webkit-user-select: none; user-select: none; /* 拖曳不選字 */
-webkit-touch-callout: none; /* 長按不跳圖片選單 */
-webkit-tap-highlight-color: transparent; /* 點擊不閃灰塊 */
}// 右鍵選單(桌機的長按等價物)
canvas.addEventListener('contextmenu', e => e.preventDefault())
// 遊戲中避免拖曳圖片
canvas.addEventListener('dragstart', e => e.preventDefault())擋不掉的東西也要知道:iOS 的邊緣返回手勢、控制中心下拉、Android 的手勢導覽列都無法用網頁攔截。設計上的因應是「不要把重要控制項放在螢幕邊緣的 20px 內」,而 env(safe-area-inset-bottom) 正好標出了 home indicator 的高度,可以直接拿來當避讓依據。
螢幕不要休眠
遊戲進行中(尤其是不需要持續觸控的類型)螢幕會自動變暗、鎖定。Screen Wake Lock API 解決這件事:
let wakeLock = null
async function keepAwake() {
if (!('wakeLock' in navigator)) return
try {
wakeLock = await navigator.wakeLock.request('screen')
wakeLock.addEventListener('release', () => { wakeLock = null })
} catch {}
}
// 切到背景會自動釋放,回來要重新請求
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && !wakeLock) keepAwake()
})需要安全上下文(HTTPS)與頁面可見。記得在遊戲暫停或離開時 wakeLock.release(),這是使用者電量的直接成本。
一個完整的「進入遊戲」流程
async function startGame() {
// 1. 盡力全螢幕(可能失敗,不阻擋流程)
const fs = await enterFullscreen(document.documentElement)
// 2. 全螢幕成功才嘗試鎖方向(規範要求)
if (fs) await lockLandscape()
// 3. 不論前兩步結果,都要有 CSS 層的方向處理當保底
// —— 這是 iPhone 上實際生效的那一層
// 4. 螢幕保持喚醒
keepAwake()
// 5. 尺寸交給 ResizeObserver,這裡不量任何東西
game.run()
}第 3 步是整段的關鍵:把 Fullscreen 與 Orientation Lock 都當作「錦上添花」,讓遊戲在兩者都失敗時依然完整可玩。 這條原則在 iPhone 佔比高的市場尤其重要。
檢查清單
-
requestFullscreen()在使用者手勢同步呼叫堆疊中,不在await之後 - 有
document.fullscreenEnabled檢查與catch - 遊戲內有明顯的退出全螢幕入口
-
screen.orientation.lock()在全螢幕成功後才呼叫,且有降級 - 有 PWA manifest,
display_override有列fullscreen - 重要控制項不放在螢幕邊緣 20px 內
- Wake Lock 有在
visibilitychange後重新請求,離開時釋放 - 實測:iPhone Safari(非 PWA)、iPhone 加到主畫面、Android Chrome、桌機
下一篇處理輸入法把版面頂爛的問題 → 鍵盤與 visualViewport