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.fullscreenEnabledfalse
  • 失敗原因要分辨。 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()」,而且兩者都可能失敗。

可鎖定的值:anynaturalportraitlandscapeportrait-primaryportrait-secondarylandscape-primarylandscape-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">

要注意的落差:

  • manifestorientation 欄位 iOS 不理會,只有 Android 的 standalone PWA 會遵守。
  • display: fullscreen 在 iOS 上被當作 standalone 處理——狀態列仍在,但工具列消失,這已經是很大的改善。
  • black-translucent 讓內容延伸到狀態列底下,此時 env(safe-area-inset-top) 就是你避開狀態列的唯一依據。
  • 偵測是否處於 standalonematchMedia('(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