[{"data":1,"prerenderedAt":1882},["ShallowReactive",2],{"search-dialog-posts":3,"post-\u002Fposts\u002Fscreen-wake-lock-api":316,"post-surround-\u002Fposts\u002Fscreen-wake-lock-api":1875},[4,12,21,29,37,47,57,67,77,85,96,104,112,121,129,137,147,155,164,173,181,188,195,207,215,224,236,241,247,254,260,266,272,279,286,291,298,304,310],{"path":5,"title":6,"description":7,"category":8,"tags":9},"\u002Fposts\u002Fgit-commands-cheatsheet","Git 常用指令與終端機筆記","2018 年整理的 Git 學習筆記，包含基本終端機指令、Git 操作、分支、reset、SourceTree、Vim 與一些常用設定。","notes",[10,11],"Git","版本控制",{"path":13,"title":14,"description":15,"category":16,"tags":17},"\u002Fposts\u002Fjs-prototype-inheritance","JavaScript 物件導向：建構函式、prototype 與繼承三步驟","整理 ES5 時代用建構函式加 prototype 手刻物件導向的完整流程——封裝屬性與方法、共用函式為什麼要掛在 prototype 上、以及繼承的三個步驟（call 借用母類別建構子、Object.create 接上原型鏈、修正 constructor 指向），並附上物件縮寫與計算屬性這兩個順便學到的 ES6 寫法。","frontend",[18,19,20],"JavaScript","物件導向","prototype",{"path":22,"title":23,"description":24,"category":16,"tags":25},"\u002Fposts\u002Fflexbox-notes","Flexbox 對齊與彈性尺寸筆記","記錄 Flexbox 的主軸與交錯軸對齊、flex-direction、flex-wrap，以及 flex-grow、flex-shrink、flex-basis 等常用屬性。",[26,27,28],"CSS","Flexbox","版面",{"path":30,"title":31,"description":32,"category":16,"tags":33},"\u002Fposts\u002Fscreen-orientation-api","screen.orientation：現在判斷與鎖定螢幕方向的做法","整理現在 JavaScript 判斷螢幕方向的方式，包括 screen.orientation、change 事件與 lock()，並說明 RWD 使用 matchMedia 或 CSS orientation 會更適合。",[18,34,35,36],"RWD","螢幕方向","PWA",{"path":38,"title":39,"description":40,"category":16,"tags":41},"\u002Fposts\u002Fpinia-vuex-state-management","Pinia \u002F Vuex 狀態管理及差異","紀錄 Vuex、Pinia 的狀態管理概念、差異與用法。",[42,43,44,45,46],"Pinia","Vuex","Vue3","狀態管理","Nuxt",{"path":48,"title":49,"description":50,"category":8,"tags":51},"\u002Fposts\u002Fpyenv-python-version-management","Python 多版本管理 pyenv","紀錄 macOS 與 Windows 使用 pyenv 管理 Python 版本的方式。",[52,53,54,55,56],"Python","pyenv","macOS","Windows","版本管理",{"path":58,"title":59,"description":60,"category":8,"tags":61},"\u002Fposts\u002Fnpm-publish-vue2-component","npm 發布流程","紀錄 Vue 2 元件的 npm 發布、dist-tag 與打包流程。",[62,63,64,65,66],"npm","Vue2","Vue CLI","套件發布","MIT",{"path":68,"title":69,"description":70,"category":8,"tags":71},"\u002Fposts\u002Fmobile-debugging-today","現在怎麼 debug 手機網頁：從裝一堆工具到幾乎不用裝","對照 2022 年那套 scoop、iTunes、remotedebug-ios-webkit-adapter 的繁瑣設定，整理現在偵錯手機網頁的做法：Android 無線偵錯、iOS 的實際選項、on-device console、Playwright 裝置模擬，以及本機 HTTPS 的處理。",[72,73,74,75,76],"debug","iOS","Android","Chrome DevTools","Playwright",{"path":78,"title":79,"description":80,"category":16,"tags":81},"\u002Fposts\u002Fplaywright-vs-cypress","E2E 測試現在選什麼：Playwright 與 Cypress 的實際差異","對照幾年前用 Cypress 的做法，說明 Playwright 為什麼變成多數新專案的預設選擇：真正的多瀏覽器支援、免費的平行執行、trace viewer 除錯，以及兩者在架構上的根本差異與各自仍然適合的場景。",[76,82,83,84],"Cypress","E2E","測試",{"path":86,"title":87,"description":88,"category":89,"tags":90},"\u002Fposts\u002Fjs-encoding-today","現在處理編碼：TextEncoder、Uint8Array 的 Base64 方法與 URLSearchParams","整理 JavaScript 現代編碼方式，使用 TextEncoder、TextDecoder 與 Uint8Array 處理 UTF-8 與 Base64，並說明 Base64URL、URLSearchParams 與 encodeURIComponent 的差異。","research",[91,92,93,94,95,18],"編碼","TextEncoder","Base64","URLSearchParams","Uint8Array",{"path":97,"title":98,"description":99,"category":16,"tags":100},"\u002Fposts\u002Fdart-sass-migration","Dart Sass 遷移筆記：node-sass 之後怎麼裝 Sass","node-sass 與 LibSass 已停止維護，現在改用 Dart Sass。整理 webpack 與 Vite 的安裝方式，以及為什麼不再像 node-sass 一樣容易遇到 Node.js 版本與原生編譯問題。",[101,102,103],"Sass","Webpack","Vite",{"path":105,"title":106,"description":107,"category":16,"tags":108},"\u002Fposts\u002Fdayjs-vue3-computed","day.js 搭配 Vue 3 的日期格式化","整理 Vue 3 日期格式化的常見做法，使用 day.js 取代舊專案常見的 moment.js，並以 method 或 computed 取代 Vue 2 filter，也補充原生 Intl.DateTimeFormat 的使用方式。",[44,109,110,111],"day.js","日期","Intl",{"path":113,"title":114,"description":115,"category":16,"tags":116},"\u002Fposts\u002Fes-modules-today","ES Module 現在的樣子：Node ESM、exports 與 import maps","整理現代 JavaScript ES Module 的常見用法，包括 Node.js 原生 ESM、package.json exports、瀏覽器 import maps、JSON import attributes，以及 Vite 動態載入的注意事項。",[117,118,119,120,103,18],"ES Module","Node.js","import maps","exports",{"path":122,"title":123,"description":124,"category":89,"tags":125},"\u002Fposts\u002Fstructured-clone","structuredClone()：終於有內建的深層拷貝","整理 JavaScript structuredClone() 的深層拷貝用法，說明 Date、Map、Set、循環參考的處理方式，以及 Function、class 與 Vue reactive Proxy 的注意事項。",[18,126,127,128],"structuredClone","深拷貝","Vue",{"path":130,"title":131,"description":132,"category":16,"tags":133},"\u002Fposts\u002Fdynamic-import","JavaScript 動態載入：import() 與 Vue 非同步元件","整理靜態 import、動態 import() 與 Vue defineAsyncComponent 的使用方式，並簡單補充 Vite import.meta.glob 的用途。",[18,134,135,103,136],"ES Modules","Vue 3","Code Splitting",{"path":138,"title":139,"description":140,"category":16,"tags":141},"\u002Fposts\u002Fscreen-wake-lock-api","Screen Wake Lock API：不用套件也能讓頁面保持亮著","整理 Screen Wake Lock API 的基本用法，說明 HTTPS、頁面可見性、切換分頁後重新取得 wake lock，以及 Vue／Nuxt 計時器使用時的注意事項。",[142,128,143,144,145,146],"Wake Lock","Nuxt4","行動裝置","計時器","Web API",{"path":148,"title":149,"description":150,"category":16,"tags":151},"\u002Fposts\u002Fhtml-meta-boilerplate-today","現在的 HTML meta 標籤怎麼寫","整理現在一個新頁面該有的 meta 標籤骨架——viewport 補上 viewport-fit=cover 因應瀏海與圓角螢幕、color-scheme 讓表單控制項跟著深色模式換色、theme-color 決定行動瀏覽器工具列顏色，以及基本的 SEO 與 Open Graph 標籤。",[152,153,154],"HTML","meta","SEO",{"path":156,"title":157,"description":158,"category":8,"tags":159},"\u002Fposts\u002Fdocker-node-postgres","把 Node + PostgreSQL 的 backend 裝進 Docker：Dockerfile、.dockerignore 與 compose","從「本機裝了就跑」到打包成容器的通用做法——Dockerfile 每一行的理由與 layer cache 為什麼決定了指令順序、.dockerignore 漏掉 node_modules 對原生模組是災難級的問題、容器之間為什麼不能用 localhost 互相找到對方，以及怎麼驗證資料真的寫進了 volume。",[160,161,118,162,163],"Docker","Docker Compose","PostgreSQL","Express",{"path":165,"title":166,"description":167,"category":16,"tags":168},"\u002Fposts\u002Fnuxt-server-proxy","Nuxt 3 \u002F 4 用 Server API 做代理與轉發","整理 Nuxt 3 與 Nuxt 4 使用 server\u002Fapi 轉發第三方 API 的方式，可以避免瀏覽器 CORS 問題、隱藏 API Key，也能在伺服器端加入快取。",[169,143,170,171,172],"Nuxt3","CORS","Nitro","API",{"path":174,"title":175,"description":176,"category":16,"tags":177},"\u002Fposts\u002Fnuxt4-project-setup","Nuxt 4 建立專案：和 Nuxt 3 有哪些不同","整理 Nuxt 4 建立專案的基本流程與目錄結構，並對照 Nuxt 3 的差異，包括 app\u002F 目錄、路徑別名、TypeScript，以及目前常用的 ESLint 與 Tailwind CSS 4 設定。",[143,44,178,179,180],"TypeScript","ESLint","Tailwind CSS 4",{"path":182,"title":183,"description":184,"category":16,"tags":185},"\u002Fposts\u002Fnuxt3-route-middleware","Nuxt 3 \u002F 4 的 Route Middleware：進頁面前先做判斷","整理 Nuxt 3 與 Nuxt 4 的 Route Middleware，用 defineNuxtRouteMiddleware 在進入頁面前檢查登入狀態，搭配 navigateTo 重新導向，並說明一般 middleware、global middleware 與 definePageMeta 的用法。",[169,143,186,187],"Middleware","驗證",{"path":189,"title":190,"description":191,"category":16,"tags":192},"\u002Fposts\u002Fnuxt-route-rules-redirect","Nuxt 3 \u002F 4 的 routeRules：重新導向與頁面設定","整理 Nuxt 3 與 Nuxt 4 的 routeRules 用法，可以依照不同網址設定重新導向、預渲染、SSR 與快取，並簡單對照 Nuxt 2 的舊做法。",[169,143,193,194],"routeRules","重新導向",{"path":196,"title":197,"description":198,"category":16,"tags":199},"\u002Fposts\u002Fios-iframe-google-login-blank-page","iOS iframe 串接 Google 登入出現空白頁：Popup 與頁面回收問題排查紀錄","記錄 iframe 內串接 Google 第三方登入時，部分 iOS 裝置因背景頁面遭到回收並重新載入，導致 Popup 登入流程中斷，以及最後透過 Redirect 分流與登入狀態接回機制處理的過程。",[73,200,201,202,203,204,205,206],"Safari","iframe","Google Login","GIS","OAuth","Popup","Redirect",{"path":208,"title":209,"description":210,"category":16,"tags":211},"\u002Fposts\u002Fios-login-tab-return-behavior","iOS 第三方登入後回不到原頁？分頁開啟關係與回頁行為實測","記錄 iOS Safari 與 Chrome 在多分頁第三方登入流程中，登入頁關閉後不一定回到原頁的實測，以及分頁開啟關係、window.close 與 window.focus 的限制。",[73,200,212,202,204,201,213,214],"Chrome","window.close","window.focus",{"path":216,"title":217,"description":218,"category":16,"tags":219},"\u002Fposts\u002Fgoogle-app-about-blank-placeholder","Google App 內建瀏覽器：about:blank 佔位頁被關閉的排查紀錄","記錄 iOS Google App 內建瀏覽器在非同步開啟外部頁面時，about:blank 佔位分頁被自動關閉，導致第三方登入或外部導轉失敗的排查與解法。",[73,220,221,222,204,223],"Google App","window.open","about:blank","WebView",{"path":225,"title":226,"description":227,"category":228,"tags":229},"\u002Fposts\u002Fclaude-code-codex-cli-init-basics","AI 相關紀錄 v1｜Claude Code \u002F Codex CLI 初始化與基本概念","從 Claude Code 與 Codex CLI 開始，整理 CLI、Coding Agent、Context、\u002Finit 與 CLAUDE.md／AGENTS.md 這幾個一開始最容易搞混的概念，以及在既有專案中的安裝與啟動方式。","ai",[230,231,232,233,234,235],"AI","Claude Code","Codex CLI","Coding Agent","CLAUDE.md","AGENTS.md",{"path":237,"title":238,"description":239,"category":228,"tags":240},"\u002Fposts\u002Fclaude-agents-md-project-guidelines","AI 相關紀錄 v2｜CLAUDE.md \u002F AGENTS.md 怎麼寫？專案規範與實際範例","整理 CLAUDE.md 與 AGENTS.md 的用途、適合記錄的專案規範，以及可直接參考的設定範例。",[230,231,232,234,235],{"path":242,"title":243,"description":244,"category":228,"tags":245},"\u002Fposts\u002Fclaude-codex-config-locations","AI 相關紀錄 v3｜Claude Code `.claude\u002F` \u002F Codex CLI 設定位置：全域與專案設定基本概要介紹","整理 Claude Code 與 Codex CLI 的使用者層級、專案層級設定位置，以及 rules、skills、agents 等目錄的用途。",[230,231,232,246],"設定檔",{"path":248,"title":249,"description":250,"category":228,"tags":251},"\u002Fposts\u002Fclaude-settings-codex-config","AI 相關紀錄 v4｜Claude Code settings \u002F Codex config 基本設定","整理 Claude Code settings.json 與 Codex config.toml 的設定層級、權限控制和常見基本設定。",[230,231,232,252,253],"settings.json","config.toml",{"path":255,"title":256,"description":257,"category":228,"tags":258},"\u002Fposts\u002Fclaude-codex-rules-structure","AI 相關紀錄 v5｜Rules：專案規則多了之後怎麼拆？","整理 Claude Code Rules 與 Codex AGENTS.md 的拆分方式，說明何時該拆、怎麼依目錄管理專案規則。",[230,231,232,259,235],"Rules",{"path":261,"title":262,"description":263,"category":228,"tags":264},"\u002Fposts\u002Fclaude-codex-slash-commands","AI 相關紀錄 v6｜Commands \u002F Slash Commands：CLI 裡的 `\u002F` 指令是什麼？","介紹 Claude Code 與 Codex CLI 的 Slash Commands，整理常用內建指令、自訂指令與 Skills 的差異。",[230,231,232,265],"Slash Commands",{"path":267,"title":268,"description":269,"category":228,"tags":270},"\u002Fposts\u002Fclaude-codex-skills","AI 相關紀錄 v7｜Skills：把重複工作流程整理起來","介紹 Claude Code 與 Codex Skills，整理適合保存成 Skill 的重複流程、基本結構與使用方式。",[230,231,232,271],"Skills",{"path":273,"title":274,"description":275,"category":228,"tags":276},"\u002Fposts\u002Fclaude-codex-agents-subagents","AI 相關紀錄 v8｜Agent \u002F Subagent：什麼時候真的需要分工？","整理 Agent、Subagent 與 Custom Agent 的用途，說明何時適合分工，以及它們和 Rules、Skills 的差異。",[230,231,232,277,278],"Agent","Subagent",{"path":280,"title":281,"description":282,"category":228,"tags":283},"\u002Fposts\u002Fmcp-basics","AI 相關紀錄 v9｜MCP：讓 AI 使用其他工具與資料","介紹 MCP 如何讓 AI 使用外部工具與資料，並以 Pencil MCP 說明實際用途、權限風險及與 Skill 的差異。",[230,284,231,232,285],"MCP","Pencil",{"path":287,"title":288,"description":289,"category":228,"tags":290},"\u002Fposts\u002Fai-coding-agent-concepts-summary","AI 相關紀錄 v10｜基本概念小結論","用一篇快速整理 CLAUDE.md、AGENTS.md、設定檔、Rules、Commands、Skills、Agent 與 MCP 的角色和差異。",[230,231,232,284,271,277],{"path":292,"title":293,"description":294,"category":228,"tags":295},"\u002Fposts\u002Fclaude-code-codex-model-selection","Claude Code 與 Codex：不同模型怎麼選？","比較 Claude Code 與 Codex 不同模型的速度、能力與成本取捨，依工作難度整理實用的選擇方向。",[230,231,296,297],"Codex","模型選擇",{"path":299,"title":300,"description":301,"category":228,"tags":302},"\u002Fposts\u002Fclaude-model-selection","Claude 模型簡單怎麼選？","整理 Claude 各模型在速度、能力與使用情境上的差異，提供從輕量工作到複雜任務的選擇方向。",[230,231,303,297],"Claude",{"path":305,"title":306,"description":307,"category":228,"tags":308},"\u002Fposts\u002Fcodex-model-selection","Codex 模型簡單怎麼選？","整理 Codex 各模型在速度、能力與使用情境上的差異，提供從簡單修改到大型端到端任務的選擇方向。",[230,296,309,297],"OpenAI",{"path":311,"title":312,"description":313,"category":228,"tags":314},"\u002Fposts\u002Fclaude-code-codex-cli-command-cheatsheet","Claude Code \u002F Codex CLI 常用指令速查表","整理 Claude Code 與 Codex CLI 的常用斜線指令、快捷操作與進階功能，方便需要時快速查找。",[230,231,232,265,315],"速查表",{"id":317,"title":139,"body":318,"category":16,"description":140,"draft":1862,"extension":1863,"image":1864,"meta":1865,"navigation":472,"path":138,"publishedAt":1866,"relatedExperiments":1867,"seo":1871,"stem":1872,"tags":1873,"updatedAt":1866,"__hash__":1874},"posts\u002Fposts\u002F20260516.screen-wake-lock-api.md",{"type":319,"value":320,"toc":1852},"minimark",[321,325,372,375,379,382,398,401,436,443,446,449,557,560,603,606,618,621,633,636,639,645,648,898,905,908,912,919,922,1401,1404,1528,1532,1535,1541,1544,1547,1589,1592,1595,1687,1690,1693,1701,1705,1708,1711,1736,1739,1742,1745,1748,1801,1804,1810,1820,1823,1848],[322,323,324],"p",{},"以前常用 nosleep.js 這類套件，透過播放隱藏影片，避免手機螢幕自動休眠。現在瀏覽器已經有正式的 Screen Wake Lock API：",[326,327,332],"pre",{"className":328,"code":329,"language":330,"meta":331,"style":331},"language-js shiki shiki-themes github-light github-dark-default","const wakeLock = await navigator.wakeLock.request('screen')\n","js","",[333,334,335],"code",{"__ignoreMap":331},[336,337,340,344,348,351,354,358,362,365,369],"span",{"class":338,"line":339},"line",1,[336,341,343],{"class":342},"sDgYj","const",[336,345,347],{"class":346},"sPgVT"," wakeLock",[336,349,350],{"class":342}," =",[336,352,353],{"class":342}," await",[336,355,357],{"class":356},"szmH1"," navigator.wakeLock.",[336,359,361],{"class":360},"sVjZL","request",[336,363,364],{"class":356},"(",[336,366,368],{"class":367},"sNbzA","'screen'",[336,370,371],{"class":356},")\n",[322,373,374],{},"不用另外安裝套件。",[376,377,378],"h2",{"id":378},"使用條件",[322,380,381],{},"使用前有幾件事要注意：",[383,384,385,389,392],"ul",{},[386,387,388],"li",{},"必須在 HTTPS 或 localhost 等 secure context",[386,390,391],{},"頁面必須處於可見狀態",[386,393,394,395],{},"瀏覽器必須支援 ",[333,396,397],{},"navigator.wakeLock",[322,399,400],{},"可以先做 feature detection：",[326,402,404],{"className":328,"code":403,"language":330,"meta":331,"style":331},"if ('wakeLock' in navigator) {\n  \u002F\u002F 支援 Screen Wake Lock API\n}\n",[333,405,406,423,430],{"__ignoreMap":331},[336,407,408,411,414,417,420],{"class":338,"line":339},[336,409,410],{"class":342},"if",[336,412,413],{"class":356}," (",[336,415,416],{"class":367},"'wakeLock'",[336,418,419],{"class":342}," in",[336,421,422],{"class":356}," navigator) {\n",[336,424,426],{"class":338,"line":425},2,[336,427,429],{"class":428},"sPyeA","  \u002F\u002F 支援 Screen Wake Lock API\n",[336,431,433],{"class":338,"line":432},3,[336,434,435],{"class":356},"}\n",[322,437,438,439,442],{},"目前 Screen Wake Lock 只有 ",[333,440,441],{},"screen"," 類型，用途是避免螢幕自動變暗或鎖定。",[376,444,445],{"id":445},"基本用法",[322,447,448],{},"取得 wake lock：",[326,450,452],{"className":328,"code":451,"language":330,"meta":331,"style":331},"let wakeLock = null\n\nasync function keepAwake() {\n  try {\n    wakeLock = await navigator.wakeLock.request('screen')\n  } catch (error) {\n    console.warn('無法取得 Wake Lock', error)\n  }\n}\n",[333,453,454,468,474,488,497,517,529,546,552],{"__ignoreMap":331},[336,455,456,459,462,465],{"class":338,"line":339},[336,457,458],{"class":342},"let",[336,460,461],{"class":356}," wakeLock ",[336,463,464],{"class":342},"=",[336,466,467],{"class":346}," null\n",[336,469,470],{"class":338,"line":425},[336,471,473],{"emptyLinePlaceholder":472},true,"\n",[336,475,476,479,482,485],{"class":338,"line":432},[336,477,478],{"class":342},"async",[336,480,481],{"class":342}," function",[336,483,484],{"class":360}," keepAwake",[336,486,487],{"class":356},"() {\n",[336,489,491,494],{"class":338,"line":490},4,[336,492,493],{"class":342},"  try",[336,495,496],{"class":356}," {\n",[336,498,500,503,505,507,509,511,513,515],{"class":338,"line":499},5,[336,501,502],{"class":356},"    wakeLock ",[336,504,464],{"class":342},[336,506,353],{"class":342},[336,508,357],{"class":356},[336,510,361],{"class":360},[336,512,364],{"class":356},[336,514,368],{"class":367},[336,516,371],{"class":356},[336,518,520,523,526],{"class":338,"line":519},6,[336,521,522],{"class":356},"  } ",[336,524,525],{"class":342},"catch",[336,527,528],{"class":356}," (error) {\n",[336,530,532,535,538,540,543],{"class":338,"line":531},7,[336,533,534],{"class":356},"    console.",[336,536,537],{"class":360},"warn",[336,539,364],{"class":356},[336,541,542],{"class":367},"'無法取得 Wake Lock'",[336,544,545],{"class":356},", error)\n",[336,547,549],{"class":338,"line":548},8,[336,550,551],{"class":356},"  }\n",[336,553,555],{"class":338,"line":554},9,[336,556,435],{"class":356},[322,558,559],{},"不需要時要主動釋放：",[326,561,563],{"className":328,"code":562,"language":330,"meta":331,"style":331},"async function allowSleep() {\n  await wakeLock?.release()\n  wakeLock = null\n}\n",[333,564,565,576,590,599],{"__ignoreMap":331},[336,566,567,569,571,574],{"class":338,"line":339},[336,568,478],{"class":342},[336,570,481],{"class":342},[336,572,573],{"class":360}," allowSleep",[336,575,487],{"class":356},[336,577,578,581,584,587],{"class":338,"line":425},[336,579,580],{"class":342},"  await",[336,582,583],{"class":356}," wakeLock?.",[336,585,586],{"class":360},"release",[336,588,589],{"class":356},"()\n",[336,591,592,595,597],{"class":338,"line":432},[336,593,594],{"class":356},"  wakeLock ",[336,596,464],{"class":342},[336,598,467],{"class":346},[336,600,601],{"class":338,"line":490},[336,602,435],{"class":356},[322,604,605],{},"例如計時器開始時取得：",[326,607,609],{"className":328,"code":608,"language":330,"meta":331,"style":331},"keepAwake()\n",[333,610,611],{"__ignoreMap":331},[336,612,613,616],{"class":338,"line":339},[336,614,615],{"class":360},"keepAwake",[336,617,589],{"class":356},[322,619,620],{},"停止時釋放：",[326,622,624],{"className":328,"code":623,"language":330,"meta":331,"style":331},"allowSleep()\n",[333,625,626],{"__ignoreMap":331},[336,627,628,631],{"class":338,"line":339},[336,629,630],{"class":360},"allowSleep",[336,632,589],{"class":356},[376,634,635],{"id":635},"切換分頁後要重新取得",[322,637,638],{},"Wake Lock 有一個很容易漏掉的地方：",[322,640,641],{},[642,643,644],"strong",{},"當頁面進入背景時，瀏覽器可能會自動釋放 wake lock。",[322,646,647],{},"因此使用者切到其他 App 或分頁，再切回來時，需要重新申請。",[326,649,651],{"className":328,"code":650,"language":330,"meta":331,"style":331},"let shouldKeepAwake = false\nlet wakeLock = null\n\nasync function keepAwake() {\n  shouldKeepAwake = true\n\n  try {\n    wakeLock = await navigator.wakeLock.request('screen')\n  } catch {\n    wakeLock = null\n  }\n}\n\nasync function allowSleep() {\n  shouldKeepAwake = false\n\n  await wakeLock?.release()\n  wakeLock = null\n}\n\ndocument.addEventListener('visibilitychange', async () => {\n  if (\n    shouldKeepAwake &&\n    document.visibilityState === 'visible'\n  ) {\n    await keepAwake()\n  }\n})\n",[333,652,653,665,675,679,689,699,703,709,727,735,744,749,754,759,770,779,784,795,804,809,814,841,850,859,871,877,887,892],{"__ignoreMap":331},[336,654,655,657,660,662],{"class":338,"line":339},[336,656,458],{"class":342},[336,658,659],{"class":356}," shouldKeepAwake ",[336,661,464],{"class":342},[336,663,664],{"class":346}," false\n",[336,666,667,669,671,673],{"class":338,"line":425},[336,668,458],{"class":342},[336,670,461],{"class":356},[336,672,464],{"class":342},[336,674,467],{"class":346},[336,676,677],{"class":338,"line":432},[336,678,473],{"emptyLinePlaceholder":472},[336,680,681,683,685,687],{"class":338,"line":490},[336,682,478],{"class":342},[336,684,481],{"class":342},[336,686,484],{"class":360},[336,688,487],{"class":356},[336,690,691,694,696],{"class":338,"line":499},[336,692,693],{"class":356},"  shouldKeepAwake ",[336,695,464],{"class":342},[336,697,698],{"class":346}," true\n",[336,700,701],{"class":338,"line":519},[336,702,473],{"emptyLinePlaceholder":472},[336,704,705,707],{"class":338,"line":531},[336,706,493],{"class":342},[336,708,496],{"class":356},[336,710,711,713,715,717,719,721,723,725],{"class":338,"line":548},[336,712,502],{"class":356},[336,714,464],{"class":342},[336,716,353],{"class":342},[336,718,357],{"class":356},[336,720,361],{"class":360},[336,722,364],{"class":356},[336,724,368],{"class":367},[336,726,371],{"class":356},[336,728,729,731,733],{"class":338,"line":554},[336,730,522],{"class":356},[336,732,525],{"class":342},[336,734,496],{"class":356},[336,736,738,740,742],{"class":338,"line":737},10,[336,739,502],{"class":356},[336,741,464],{"class":342},[336,743,467],{"class":346},[336,745,747],{"class":338,"line":746},11,[336,748,551],{"class":356},[336,750,752],{"class":338,"line":751},12,[336,753,435],{"class":356},[336,755,757],{"class":338,"line":756},13,[336,758,473],{"emptyLinePlaceholder":472},[336,760,762,764,766,768],{"class":338,"line":761},14,[336,763,478],{"class":342},[336,765,481],{"class":342},[336,767,573],{"class":360},[336,769,487],{"class":356},[336,771,773,775,777],{"class":338,"line":772},15,[336,774,693],{"class":356},[336,776,464],{"class":342},[336,778,664],{"class":346},[336,780,782],{"class":338,"line":781},16,[336,783,473],{"emptyLinePlaceholder":472},[336,785,787,789,791,793],{"class":338,"line":786},17,[336,788,580],{"class":342},[336,790,583],{"class":356},[336,792,586],{"class":360},[336,794,589],{"class":356},[336,796,798,800,802],{"class":338,"line":797},18,[336,799,594],{"class":356},[336,801,464],{"class":342},[336,803,467],{"class":346},[336,805,807],{"class":338,"line":806},19,[336,808,435],{"class":356},[336,810,812],{"class":338,"line":811},20,[336,813,473],{"emptyLinePlaceholder":472},[336,815,817,820,823,825,828,831,833,836,839],{"class":338,"line":816},21,[336,818,819],{"class":356},"document.",[336,821,822],{"class":360},"addEventListener",[336,824,364],{"class":356},[336,826,827],{"class":367},"'visibilitychange'",[336,829,830],{"class":356},", ",[336,832,478],{"class":342},[336,834,835],{"class":356}," () ",[336,837,838],{"class":342},"=>",[336,840,496],{"class":356},[336,842,844,847],{"class":338,"line":843},22,[336,845,846],{"class":342},"  if",[336,848,849],{"class":356}," (\n",[336,851,853,856],{"class":338,"line":852},23,[336,854,855],{"class":356},"    shouldKeepAwake ",[336,857,858],{"class":342},"&&\n",[336,860,862,865,868],{"class":338,"line":861},24,[336,863,864],{"class":356},"    document.visibilityState ",[336,866,867],{"class":342},"===",[336,869,870],{"class":367}," 'visible'\n",[336,872,874],{"class":338,"line":873},25,[336,875,876],{"class":356},"  ) {\n",[336,878,880,883,885],{"class":338,"line":879},26,[336,881,882],{"class":342},"    await",[336,884,484],{"class":360},[336,886,589],{"class":356},[336,888,890],{"class":338,"line":889},27,[336,891,551],{"class":356},[336,893,895],{"class":338,"line":894},28,[336,896,897],{"class":356},"})\n",[322,899,900,901,904],{},"這裡另外使用 ",[333,902,903],{},"shouldKeepAwake"," 記錄「使用者是否仍然希望保持亮著」。",[322,906,907],{},"否則如果使用者已經停止計時器，切一次分頁回來又重新取得 wake lock，就會變成錯誤的行為。",[376,909,911],{"id":910},"nuxt-vue-使用","Nuxt \u002F Vue 使用",[322,913,914,915,918],{},"Nuxt 有 SSR，所以不能在伺服器端直接存取 ",[333,916,917],{},"navigator","。",[322,920,921],{},"可以簡單包成 composable：",[326,923,927],{"className":924,"code":925,"language":926,"meta":331,"style":331},"language-ts shiki shiki-themes github-light github-dark-default","\u002F\u002F app\u002Fcomposables\u002FuseWakeLock.ts\n\nexport function useWakeLock() {\n  const isActive = ref(false)\n  let wakeLock: WakeLockSentinel | null = null\n\n  async function request() {\n    if (\n      !import.meta.client ||\n      !('wakeLock' in navigator)\n    ) {\n      return\n    }\n\n    try {\n      wakeLock = await navigator.wakeLock.request('screen')\n      isActive.value = true\n    } catch {\n      isActive.value = false\n    }\n  }\n\n  async function release() {\n    isActive.value = false\n\n    await wakeLock?.release()\n    wakeLock = null\n  }\n\n  async function handleVisibilityChange() {\n    if (\n      isActive.value &&\n      document.visibilityState === 'visible'\n    ) {\n      await request()\n    }\n  }\n\n  onMounted(() => {\n    document.addEventListener(\n      'visibilitychange',\n      handleVisibilityChange\n    )\n  })\n\n  onUnmounted(() => {\n    document.removeEventListener(\n      'visibilitychange',\n      handleVisibilityChange\n    )\n\n    release()\n  })\n\n  return {\n    isActive,\n    request,\n    release\n  }\n}\n","ts",[333,928,929,934,938,950,970,994,998,1010,1017,1033,1047,1052,1057,1062,1066,1073,1092,1101,1110,1118,1122,1126,1130,1141,1150,1154,1164,1172,1176,1181,1193,1200,1207,1217,1222,1232,1237,1242,1247,1260,1271,1280,1286,1292,1298,1303,1315,1325,1332,1337,1342,1347,1355,1360,1365,1373,1379,1385,1391,1396],{"__ignoreMap":331},[336,930,931],{"class":338,"line":339},[336,932,933],{"class":428},"\u002F\u002F app\u002Fcomposables\u002FuseWakeLock.ts\n",[336,935,936],{"class":338,"line":425},[336,937,473],{"emptyLinePlaceholder":472},[336,939,940,943,945,948],{"class":338,"line":432},[336,941,942],{"class":342},"export",[336,944,481],{"class":342},[336,946,947],{"class":360}," useWakeLock",[336,949,487],{"class":356},[336,951,952,955,958,960,963,965,968],{"class":338,"line":490},[336,953,954],{"class":342},"  const",[336,956,957],{"class":346}," isActive",[336,959,350],{"class":342},[336,961,962],{"class":360}," ref",[336,964,364],{"class":356},[336,966,967],{"class":346},"false",[336,969,371],{"class":356},[336,971,972,975,977,980,984,987,990,992],{"class":338,"line":499},[336,973,974],{"class":342},"  let",[336,976,347],{"class":356},[336,978,979],{"class":342},":",[336,981,983],{"class":982},"sYRkA"," WakeLockSentinel",[336,985,986],{"class":342}," |",[336,988,989],{"class":346}," null",[336,991,350],{"class":342},[336,993,467],{"class":346},[336,995,996],{"class":338,"line":519},[336,997,473],{"emptyLinePlaceholder":472},[336,999,1000,1003,1005,1008],{"class":338,"line":531},[336,1001,1002],{"class":342},"  async",[336,1004,481],{"class":342},[336,1006,1007],{"class":360}," request",[336,1009,487],{"class":356},[336,1011,1012,1015],{"class":338,"line":548},[336,1013,1014],{"class":342},"    if",[336,1016,849],{"class":356},[336,1018,1019,1022,1025,1027,1030],{"class":338,"line":554},[336,1020,1021],{"class":342},"      !import",[336,1023,1024],{"class":356},".",[336,1026,153],{"class":346},[336,1028,1029],{"class":356},".client ",[336,1031,1032],{"class":342},"||\n",[336,1034,1035,1038,1040,1042,1044],{"class":338,"line":737},[336,1036,1037],{"class":342},"      !",[336,1039,364],{"class":356},[336,1041,416],{"class":367},[336,1043,419],{"class":342},[336,1045,1046],{"class":356}," navigator)\n",[336,1048,1049],{"class":338,"line":746},[336,1050,1051],{"class":356},"    ) {\n",[336,1053,1054],{"class":338,"line":751},[336,1055,1056],{"class":342},"      return\n",[336,1058,1059],{"class":338,"line":756},[336,1060,1061],{"class":356},"    }\n",[336,1063,1064],{"class":338,"line":761},[336,1065,473],{"emptyLinePlaceholder":472},[336,1067,1068,1071],{"class":338,"line":772},[336,1069,1070],{"class":342},"    try",[336,1072,496],{"class":356},[336,1074,1075,1078,1080,1082,1084,1086,1088,1090],{"class":338,"line":781},[336,1076,1077],{"class":356},"      wakeLock ",[336,1079,464],{"class":342},[336,1081,353],{"class":342},[336,1083,357],{"class":356},[336,1085,361],{"class":360},[336,1087,364],{"class":356},[336,1089,368],{"class":367},[336,1091,371],{"class":356},[336,1093,1094,1097,1099],{"class":338,"line":786},[336,1095,1096],{"class":356},"      isActive.value ",[336,1098,464],{"class":342},[336,1100,698],{"class":346},[336,1102,1103,1106,1108],{"class":338,"line":797},[336,1104,1105],{"class":356},"    } ",[336,1107,525],{"class":342},[336,1109,496],{"class":356},[336,1111,1112,1114,1116],{"class":338,"line":806},[336,1113,1096],{"class":356},[336,1115,464],{"class":342},[336,1117,664],{"class":346},[336,1119,1120],{"class":338,"line":811},[336,1121,1061],{"class":356},[336,1123,1124],{"class":338,"line":816},[336,1125,551],{"class":356},[336,1127,1128],{"class":338,"line":843},[336,1129,473],{"emptyLinePlaceholder":472},[336,1131,1132,1134,1136,1139],{"class":338,"line":852},[336,1133,1002],{"class":342},[336,1135,481],{"class":342},[336,1137,1138],{"class":360}," release",[336,1140,487],{"class":356},[336,1142,1143,1146,1148],{"class":338,"line":861},[336,1144,1145],{"class":356},"    isActive.value ",[336,1147,464],{"class":342},[336,1149,664],{"class":346},[336,1151,1152],{"class":338,"line":873},[336,1153,473],{"emptyLinePlaceholder":472},[336,1155,1156,1158,1160,1162],{"class":338,"line":879},[336,1157,882],{"class":342},[336,1159,583],{"class":356},[336,1161,586],{"class":360},[336,1163,589],{"class":356},[336,1165,1166,1168,1170],{"class":338,"line":889},[336,1167,502],{"class":356},[336,1169,464],{"class":342},[336,1171,467],{"class":346},[336,1173,1174],{"class":338,"line":894},[336,1175,551],{"class":356},[336,1177,1179],{"class":338,"line":1178},29,[336,1180,473],{"emptyLinePlaceholder":472},[336,1182,1184,1186,1188,1191],{"class":338,"line":1183},30,[336,1185,1002],{"class":342},[336,1187,481],{"class":342},[336,1189,1190],{"class":360}," handleVisibilityChange",[336,1192,487],{"class":356},[336,1194,1196,1198],{"class":338,"line":1195},31,[336,1197,1014],{"class":342},[336,1199,849],{"class":356},[336,1201,1203,1205],{"class":338,"line":1202},32,[336,1204,1096],{"class":356},[336,1206,858],{"class":342},[336,1208,1210,1213,1215],{"class":338,"line":1209},33,[336,1211,1212],{"class":356},"      document.visibilityState ",[336,1214,867],{"class":342},[336,1216,870],{"class":367},[336,1218,1220],{"class":338,"line":1219},34,[336,1221,1051],{"class":356},[336,1223,1225,1228,1230],{"class":338,"line":1224},35,[336,1226,1227],{"class":342},"      await",[336,1229,1007],{"class":360},[336,1231,589],{"class":356},[336,1233,1235],{"class":338,"line":1234},36,[336,1236,1061],{"class":356},[336,1238,1240],{"class":338,"line":1239},37,[336,1241,551],{"class":356},[336,1243,1245],{"class":338,"line":1244},38,[336,1246,473],{"emptyLinePlaceholder":472},[336,1248,1250,1253,1256,1258],{"class":338,"line":1249},39,[336,1251,1252],{"class":360},"  onMounted",[336,1254,1255],{"class":356},"(() ",[336,1257,838],{"class":342},[336,1259,496],{"class":356},[336,1261,1263,1266,1268],{"class":338,"line":1262},40,[336,1264,1265],{"class":356},"    document.",[336,1267,822],{"class":360},[336,1269,1270],{"class":356},"(\n",[336,1272,1274,1277],{"class":338,"line":1273},41,[336,1275,1276],{"class":367},"      'visibilitychange'",[336,1278,1279],{"class":356},",\n",[336,1281,1283],{"class":338,"line":1282},42,[336,1284,1285],{"class":356},"      handleVisibilityChange\n",[336,1287,1289],{"class":338,"line":1288},43,[336,1290,1291],{"class":356},"    )\n",[336,1293,1295],{"class":338,"line":1294},44,[336,1296,1297],{"class":356},"  })\n",[336,1299,1301],{"class":338,"line":1300},45,[336,1302,473],{"emptyLinePlaceholder":472},[336,1304,1306,1309,1311,1313],{"class":338,"line":1305},46,[336,1307,1308],{"class":360},"  onUnmounted",[336,1310,1255],{"class":356},[336,1312,838],{"class":342},[336,1314,496],{"class":356},[336,1316,1318,1320,1323],{"class":338,"line":1317},47,[336,1319,1265],{"class":356},[336,1321,1322],{"class":360},"removeEventListener",[336,1324,1270],{"class":356},[336,1326,1328,1330],{"class":338,"line":1327},48,[336,1329,1276],{"class":367},[336,1331,1279],{"class":356},[336,1333,1335],{"class":338,"line":1334},49,[336,1336,1285],{"class":356},[336,1338,1340],{"class":338,"line":1339},50,[336,1341,1291],{"class":356},[336,1343,1345],{"class":338,"line":1344},51,[336,1346,473],{"emptyLinePlaceholder":472},[336,1348,1350,1353],{"class":338,"line":1349},52,[336,1351,1352],{"class":360},"    release",[336,1354,589],{"class":356},[336,1356,1358],{"class":338,"line":1357},53,[336,1359,1297],{"class":356},[336,1361,1363],{"class":338,"line":1362},54,[336,1364,473],{"emptyLinePlaceholder":472},[336,1366,1368,1371],{"class":338,"line":1367},55,[336,1369,1370],{"class":342},"  return",[336,1372,496],{"class":356},[336,1374,1376],{"class":338,"line":1375},56,[336,1377,1378],{"class":356},"    isActive,\n",[336,1380,1382],{"class":338,"line":1381},57,[336,1383,1384],{"class":356},"    request,\n",[336,1386,1388],{"class":338,"line":1387},58,[336,1389,1390],{"class":356},"    release\n",[336,1392,1394],{"class":338,"line":1393},59,[336,1395,551],{"class":356},[336,1397,1399],{"class":338,"line":1398},60,[336,1400,435],{"class":356},[322,1402,1403],{},"使用：",[326,1405,1409],{"className":1406,"code":1407,"language":1408,"meta":331,"style":331},"language-vue shiki shiki-themes github-light github-dark-default","\u003Cscript setup lang=\"ts\">\nconst {\n  isActive,\n  request,\n  release\n} = useWakeLock()\n\nfunction startTimer() {\n  request()\n}\n\nfunction stopTimer() {\n  release()\n}\n\u003C\u002Fscript>\n","vue",[333,1410,1411,1435,1441,1448,1455,1460,1471,1475,1485,1491,1495,1499,1508,1515,1519],{"__ignoreMap":331},[336,1412,1413,1416,1420,1424,1427,1429,1432],{"class":338,"line":339},[336,1414,1415],{"class":356},"\u003C",[336,1417,1419],{"class":1418},"sXQ1o","script",[336,1421,1423],{"class":1422},"syv4q"," setup",[336,1425,1426],{"class":1422}," lang",[336,1428,464],{"class":356},[336,1430,1431],{"class":367},"\"ts\"",[336,1433,1434],{"class":356},">\n",[336,1436,1437,1439],{"class":338,"line":425},[336,1438,343],{"class":342},[336,1440,496],{"class":356},[336,1442,1443,1446],{"class":338,"line":432},[336,1444,1445],{"class":346},"  isActive",[336,1447,1279],{"class":356},[336,1449,1450,1453],{"class":338,"line":490},[336,1451,1452],{"class":346},"  request",[336,1454,1279],{"class":356},[336,1456,1457],{"class":338,"line":499},[336,1458,1459],{"class":346},"  release\n",[336,1461,1462,1465,1467,1469],{"class":338,"line":519},[336,1463,1464],{"class":356},"} ",[336,1466,464],{"class":342},[336,1468,947],{"class":360},[336,1470,589],{"class":356},[336,1472,1473],{"class":338,"line":531},[336,1474,473],{"emptyLinePlaceholder":472},[336,1476,1477,1480,1483],{"class":338,"line":548},[336,1478,1479],{"class":342},"function",[336,1481,1482],{"class":360}," startTimer",[336,1484,487],{"class":356},[336,1486,1487,1489],{"class":338,"line":554},[336,1488,1452],{"class":360},[336,1490,589],{"class":356},[336,1492,1493],{"class":338,"line":737},[336,1494,435],{"class":356},[336,1496,1497],{"class":338,"line":746},[336,1498,473],{"emptyLinePlaceholder":472},[336,1500,1501,1503,1506],{"class":338,"line":751},[336,1502,1479],{"class":342},[336,1504,1505],{"class":360}," stopTimer",[336,1507,487],{"class":356},[336,1509,1510,1513],{"class":338,"line":756},[336,1511,1512],{"class":360},"  release",[336,1514,589],{"class":356},[336,1516,1517],{"class":338,"line":761},[336,1518,435],{"class":356},[336,1520,1521,1524,1526],{"class":338,"line":772},[336,1522,1523],{"class":356},"\u003C\u002F",[336,1525,1419],{"class":1418},[336,1527,1434],{"class":356},[376,1529,1531],{"id":1530},"wake-lock-不代表計時器一定準","Wake Lock 不代表計時器一定準",[322,1533,1534],{},"Screen Wake Lock 的功能只有：",[1536,1537,1538],"blockquote",{},[322,1539,1540],{},"防止螢幕自動變暗或鎖定。",[322,1542,1543],{},"它不會保證背景分頁裡的 JavaScript 持續每秒執行。",[322,1545,1546],{},"因此計時器不要單純這樣累加：",[326,1548,1550],{"className":328,"code":1549,"language":330,"meta":331,"style":331},"\u002F\u002F 不建議\nsetInterval(() => {\n  remaining -= 1\n}, 1000)\n",[333,1551,1552,1557,1568,1579],{"__ignoreMap":331},[336,1553,1554],{"class":338,"line":339},[336,1555,1556],{"class":428},"\u002F\u002F 不建議\n",[336,1558,1559,1562,1564,1566],{"class":338,"line":425},[336,1560,1561],{"class":360},"setInterval",[336,1563,1255],{"class":356},[336,1565,838],{"class":342},[336,1567,496],{"class":356},[336,1569,1570,1573,1576],{"class":338,"line":432},[336,1571,1572],{"class":356},"  remaining ",[336,1574,1575],{"class":342},"-=",[336,1577,1578],{"class":346}," 1\n",[336,1580,1581,1584,1587],{"class":338,"line":490},[336,1582,1583],{"class":356},"}, ",[336,1585,1586],{"class":346},"1000",[336,1588,371],{"class":356},[322,1590,1591],{},"背景分頁的 timer 可能被瀏覽器降頻。",[322,1593,1594],{},"比較安全的方式是記錄結束時間：",[326,1596,1598],{"className":328,"code":1597,"language":330,"meta":331,"style":331},"const endAt = Date.now() + duration\n\nsetInterval(() => {\n  remaining = Math.max(\n    0,\n    endAt - Date.now()\n  )\n}, 250)\n",[333,1599,1600,1624,1628,1638,1652,1659,1673,1678],{"__ignoreMap":331},[336,1601,1602,1604,1607,1609,1612,1615,1618,1621],{"class":338,"line":339},[336,1603,343],{"class":342},[336,1605,1606],{"class":346}," endAt",[336,1608,350],{"class":342},[336,1610,1611],{"class":356}," Date.",[336,1613,1614],{"class":360},"now",[336,1616,1617],{"class":356},"() ",[336,1619,1620],{"class":342},"+",[336,1622,1623],{"class":356}," duration\n",[336,1625,1626],{"class":338,"line":425},[336,1627,473],{"emptyLinePlaceholder":472},[336,1629,1630,1632,1634,1636],{"class":338,"line":432},[336,1631,1561],{"class":360},[336,1633,1255],{"class":356},[336,1635,838],{"class":342},[336,1637,496],{"class":356},[336,1639,1640,1642,1644,1647,1650],{"class":338,"line":490},[336,1641,1572],{"class":356},[336,1643,464],{"class":342},[336,1645,1646],{"class":356}," Math.",[336,1648,1649],{"class":360},"max",[336,1651,1270],{"class":356},[336,1653,1654,1657],{"class":338,"line":499},[336,1655,1656],{"class":346},"    0",[336,1658,1279],{"class":356},[336,1660,1661,1664,1667,1669,1671],{"class":338,"line":519},[336,1662,1663],{"class":356},"    endAt ",[336,1665,1666],{"class":342},"-",[336,1668,1611],{"class":356},[336,1670,1614],{"class":360},[336,1672,589],{"class":356},[336,1674,1675],{"class":338,"line":531},[336,1676,1677],{"class":356},"  )\n",[336,1679,1680,1682,1685],{"class":338,"line":548},[336,1681,1583],{"class":356},[336,1683,1684],{"class":346},"250",[336,1686,371],{"class":356},[322,1688,1689],{},"即使中間更新被暫停，重新執行時仍然會根據目前時間計算正確的剩餘時間。",[322,1691,1692],{},"所以兩件事要分開理解：",[326,1694,1699],{"className":1695,"code":1697,"language":1698,"meta":331},[1696],"language-text","Screen Wake Lock\n→ 讓螢幕保持亮著\n\n時間戳\n→ 讓計時結果保持正確\n","text",[333,1700,1697],{"__ignoreMap":331},[376,1702,1704],{"id":1703},"還需要-nosleepjs-嗎","還需要 nosleep.js 嗎？",[322,1706,1707],{},"現代瀏覽器可以優先使用原生 Screen Wake Lock API。",[322,1709,1710],{},"建議做 feature detection：",[326,1712,1714],{"className":328,"code":1713,"language":330,"meta":331,"style":331},"const isSupported =\n  'wakeLock' in navigator\n",[333,1715,1716,1726],{"__ignoreMap":331},[336,1717,1718,1720,1723],{"class":338,"line":339},[336,1719,343],{"class":342},[336,1721,1722],{"class":346}," isSupported",[336,1724,1725],{"class":342}," =\n",[336,1727,1728,1731,1733],{"class":338,"line":425},[336,1729,1730],{"class":367},"  'wakeLock'",[336,1732,419],{"class":342},[336,1734,1735],{"class":356}," navigator\n",[322,1737,1738],{},"如果不支援，讓功能正常降級即可。",[322,1740,1741],{},"對倒數計時器來說，Wake Lock 通常只是額外的使用體驗，不應該影響計時器本身能不能使用。",[376,1743,1744],{"id":1744},"小結",[322,1746,1747],{},"使用 Screen Wake Lock API 最重要的是記住：",[326,1749,1751],{"className":328,"code":1750,"language":330,"meta":331,"style":331},"\u002F\u002F 保持螢幕亮著\nconst wakeLock =\n  await navigator.wakeLock.request('screen')\n\n\u002F\u002F 恢復正常休眠\nawait wakeLock.release()\n",[333,1752,1753,1758,1766,1780,1784,1789],{"__ignoreMap":331},[336,1754,1755],{"class":338,"line":339},[336,1756,1757],{"class":428},"\u002F\u002F 保持螢幕亮著\n",[336,1759,1760,1762,1764],{"class":338,"line":425},[336,1761,343],{"class":342},[336,1763,347],{"class":346},[336,1765,1725],{"class":342},[336,1767,1768,1770,1772,1774,1776,1778],{"class":338,"line":432},[336,1769,580],{"class":342},[336,1771,357],{"class":356},[336,1773,361],{"class":360},[336,1775,364],{"class":356},[336,1777,368],{"class":367},[336,1779,371],{"class":356},[336,1781,1782],{"class":338,"line":490},[336,1783,473],{"emptyLinePlaceholder":472},[336,1785,1786],{"class":338,"line":499},[336,1787,1788],{"class":428},"\u002F\u002F 恢復正常休眠\n",[336,1790,1791,1794,1797,1799],{"class":338,"line":519},[336,1792,1793],{"class":342},"await",[336,1795,1796],{"class":356}," wakeLock.",[336,1798,586],{"class":360},[336,1800,589],{"class":356},[322,1802,1803],{},"另外兩個常見注意事項：",[326,1805,1808],{"className":1806,"code":1807,"language":1698,"meta":331},[1696],"切到背景\n→ wake lock 可能被自動釋放\n→ 回到頁面後重新 request()\n\n計時器\n→ 不要依賴 setInterval 累加\n→ 用時間戳重新計算\n",[333,1809,1807],{"__ignoreMap":331},[322,1811,1812,1813,1815,1816,1819],{},"在 Nuxt 裡再注意 SSR 階段沒有 ",[333,1814,917],{},"，使用 ",[333,1817,1818],{},"import.meta.client"," 或在 client lifecycle 裡處理即可。",[376,1821,1822],{"id":1822},"參考資料",[383,1824,1825,1834,1841],{},[386,1826,1827],{},[1828,1829,1833],"a",{"href":1830,"rel":1831},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FScreen_Wake_Lock_API",[1832],"nofollow","MDN：Screen Wake Lock API",[386,1835,1836],{},[1828,1837,1840],{"href":1838,"rel":1839},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWakeLock\u002Frequest",[1832],"MDN：WakeLock.request()",[386,1842,1843],{},[1828,1844,1847],{"href":1845,"rel":1846},"https:\u002F\u002Fwww.w3.org\u002FTR\u002Fscreen-wake-lock\u002F",[1832],"W3C：Screen Wake Lock API",[1849,1850,1851],"style",{},"html pre.shiki code .sDgYj, html code.shiki .sDgYj{--shiki-default:#D73A49;--shiki-dark:#FF7B72}html pre.shiki code .sPgVT, html code.shiki .sPgVT{--shiki-default:#005CC5;--shiki-dark:#79C0FF}html pre.shiki code .szmH1, html code.shiki .szmH1{--shiki-default:#24292E;--shiki-dark:#E6EDF3}html pre.shiki code .sVjZL, html code.shiki .sVjZL{--shiki-default:#6F42C1;--shiki-dark:#D2A8FF}html pre.shiki code .sNbzA, html code.shiki .sNbzA{--shiki-default:#032F62;--shiki-dark:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sPyeA, html code.shiki .sPyeA{--shiki-default:#6A737D;--shiki-dark:#8B949E}html pre.shiki code .sYRkA, html code.shiki .sYRkA{--shiki-default:#6F42C1;--shiki-dark:#FFA657}html pre.shiki code .sXQ1o, html code.shiki .sXQ1o{--shiki-default:#22863A;--shiki-dark:#7EE787}html pre.shiki code .syv4q, html code.shiki .syv4q{--shiki-default:#6F42C1;--shiki-dark:#79C0FF}",{"title":331,"searchDepth":432,"depth":432,"links":1853},[1854,1855,1856,1857,1858,1859,1860,1861],{"id":378,"depth":425,"text":378},{"id":445,"depth":425,"text":445},{"id":635,"depth":425,"text":635},{"id":910,"depth":425,"text":911},{"id":1530,"depth":425,"text":1531},{"id":1703,"depth":425,"text":1704},{"id":1744,"depth":425,"text":1744},{"id":1822,"depth":425,"text":1822},false,"md",null,{},"2026-06-16",[1868,1869,1870],"pomodoro-timer","countdown-timer","stopwatch",{"title":139,"description":140},"posts\u002F20260516.screen-wake-lock-api",[142,128,143,144,145,146],"P7PKRA5z9bQnUlNdAg9zjwHWtKHVL4u7gizToX6mlrU",[1876,1879],{"title":166,"path":165,"stem":1877,"description":167,"category":16,"publishedAt":1878,"children":-1},"posts\u002F20260619.nuxt-server-proxy","2026-06-19",{"title":157,"path":156,"stem":1880,"description":158,"category":8,"publishedAt":1881,"children":-1},"posts\u002F20260613.docker-node-postgres","2026-06-13",1791786901078]