跳到主要內容
前端開發

Screen Wake Lock API:不用套件也能讓頁面保持亮著

整理 Screen Wake Lock API 的基本用法,說明 HTTPS、頁面可見性、切換分頁後重新取得 wake lock,以及 Vue/Nuxt 計時器使用時的注意事項。

W

以前常用 nosleep.js 這類套件,透過播放隱藏影片,避免手機螢幕自動休眠。現在瀏覽器已經有正式的 Screen Wake Lock API:

const wakeLock = await navigator.wakeLock.request('screen')

不用另外安裝套件。

使用條件

使用前有幾件事要注意:

  • 必須在 HTTPS 或 localhost 等 secure context
  • 頁面必須處於可見狀態
  • 瀏覽器必須支援 navigator.wakeLock

可以先做 feature detection:

if ('wakeLock' in navigator) {
  // 支援 Screen Wake Lock API
}

目前 Screen Wake Lock 只有 screen 類型,用途是避免螢幕自動變暗或鎖定。

基本用法

取得 wake lock:

let wakeLock = null

async function keepAwake() {
  try {
    wakeLock = await navigator.wakeLock.request('screen')
  } catch (error) {
    console.warn('無法取得 Wake Lock', error)
  }
}

不需要時要主動釋放:

async function allowSleep() {
  await wakeLock?.release()
  wakeLock = null
}

例如計時器開始時取得:

keepAwake()

停止時釋放:

allowSleep()

切換分頁後要重新取得

Wake Lock 有一個很容易漏掉的地方:

當頁面進入背景時,瀏覽器可能會自動釋放 wake lock。

因此使用者切到其他 App 或分頁,再切回來時,需要重新申請。

let shouldKeepAwake = false
let wakeLock = null

async function keepAwake() {
  shouldKeepAwake = true

  try {
    wakeLock = await navigator.wakeLock.request('screen')
  } catch {
    wakeLock = null
  }
}

async function allowSleep() {
  shouldKeepAwake = false

  await wakeLock?.release()
  wakeLock = null
}

document.addEventListener('visibilitychange', async () => {
  if (
    shouldKeepAwake &&
    document.visibilityState === 'visible'
  ) {
    await keepAwake()
  }
})

這裡另外使用 shouldKeepAwake 記錄「使用者是否仍然希望保持亮著」。

否則如果使用者已經停止計時器,切一次分頁回來又重新取得 wake lock,就會變成錯誤的行為。

Nuxt / Vue 使用

Nuxt 有 SSR,所以不能在伺服器端直接存取 navigator。

可以簡單包成 composable:

// app/composables/useWakeLock.ts

export function useWakeLock() {
  const isActive = ref(false)
  let wakeLock: WakeLockSentinel | null = null

  async function request() {
    if (
      !import.meta.client ||
      !('wakeLock' in navigator)
    ) {
      return
    }

    try {
      wakeLock = await navigator.wakeLock.request('screen')
      isActive.value = true
    } catch {
      isActive.value = false
    }
  }

  async function release() {
    isActive.value = false

    await wakeLock?.release()
    wakeLock = null
  }

  async function handleVisibilityChange() {
    if (
      isActive.value &&
      document.visibilityState === 'visible'
    ) {
      await request()
    }
  }

  onMounted(() => {
    document.addEventListener(
      'visibilitychange',
      handleVisibilityChange
    )
  })

  onUnmounted(() => {
    document.removeEventListener(
      'visibilitychange',
      handleVisibilityChange
    )

    release()
  })

  return {
    isActive,
    request,
    release
  }
}

使用:

<script setup lang="ts">
const {
  isActive,
  request,
  release
} = useWakeLock()

function startTimer() {
  request()
}

function stopTimer() {
  release()
}
</script>

Wake Lock 不代表計時器一定準

Screen Wake Lock 的功能只有:

防止螢幕自動變暗或鎖定。

它不會保證背景分頁裡的 JavaScript 持續每秒執行。

因此計時器不要單純這樣累加:

// 不建議
setInterval(() => {
  remaining -= 1
}, 1000)

背景分頁的 timer 可能被瀏覽器降頻。

比較安全的方式是記錄結束時間:

const endAt = Date.now() + duration

setInterval(() => {
  remaining = Math.max(
    0,
    endAt - Date.now()
  )
}, 250)

即使中間更新被暫停,重新執行時仍然會根據目前時間計算正確的剩餘時間。

所以兩件事要分開理解:

Screen Wake Lock
→ 讓螢幕保持亮著

時間戳
→ 讓計時結果保持正確

還需要 nosleep.js 嗎?

現代瀏覽器可以優先使用原生 Screen Wake Lock API。

建議做 feature detection:

const isSupported =
  'wakeLock' in navigator

如果不支援,讓功能正常降級即可。

對倒數計時器來說,Wake Lock 通常只是額外的使用體驗,不應該影響計時器本身能不能使用。

小結

使用 Screen Wake Lock API 最重要的是記住:

// 保持螢幕亮著
const wakeLock =
  await navigator.wakeLock.request('screen')

// 恢復正常休眠
await wakeLock.release()

另外兩個常見注意事項:

切到背景
→ wake lock 可能被自動釋放
→ 回到頁面後重新 request()

計時器
→ 不要依賴 setInterval 累加
→ 用時間戳重新計算

在 Nuxt 裡再注意 SSR 階段沒有 navigator,使用 import.meta.client 或在 client lifecycle 裡處理即可。

參考資料

分享這篇