人睡眠工具的實(shí)現(xiàn)思路)
1. 先說結(jié)論這個(gè)“騙自己睡覺”的 App核心不是技術(shù)而是幫你建立睡前儀式感很多人看到“騙自己睡覺”幾個(gè)字以為是一個(gè)看時(shí)間、播放白噪音、或者定時(shí)鎖屏的普通工具。但實(shí)際做完之后你會(huì)發(fā)現(xiàn)真正起作用的關(guān)鍵不在“提醒”而在“騙”。它要解決的是一個(gè)很真實(shí)的問題睡前的你并不是缺少困意而是大腦還在高速運(yùn)轉(zhuǎn)不愿意進(jìn)入休息狀態(tài)。我把它理解成一個(gè)“睡前儀式感生成器”。它不是強(qiáng)制你放下手機(jī)而是用一套可復(fù)現(xiàn)的流程讓你的身體和大腦在固定路徑里慢慢松弛下來。說得直白一點(diǎn)它不是鬧鐘不是助眠電臺(tái)更像是一個(gè)“睡前觸發(fā)器”。這期內(nèi)容適合幾類人看睡前總是忍不住刷短視頻、回消息、看新聞越看越清醒的人想用 App 記錄睡眠規(guī)律但又不想買手環(huán)、不想搞復(fù)雜智能硬件的人想自己做一個(gè)簡單 App 練手又不想碰復(fù)雜前端框架、不想買服務(wù)器的人對(duì)“本地優(yōu)先”“離線可用”這類方案感興趣希望一個(gè)應(yīng)用只服務(wù)自己一個(gè)人的開發(fā)者。我一直覺得個(gè)人工具類 App 最值得做的不是大而全而是“它只為你一個(gè)人工作”。這個(gè)“騙自己睡覺”的 App 恰好就是這種定位?;氐秸}我先把“騙自己睡覺”拆成三個(gè)實(shí)際功能睡前倒計(jì)時(shí)設(shè)定一個(gè)“入睡準(zhǔn)備時(shí)間”比如 30 分鐘到點(diǎn)后進(jìn)入“睡眠保護(hù)模式”。漸進(jìn)式提示不是到點(diǎn)突然彈窗叫你睡覺而是在這段時(shí)間里用弱化視覺、低干擾提示、柔和反饋把你的注意力從手機(jī)上逐步移開。睡前打卡記錄記錄你實(shí)際從準(zhǔn)備到入睡的周期幫助你觀察自己到底是“真正困了”還是“只是習(xí)慣了熬夜”。從這三個(gè)功能出發(fā)你就能理解接下來的實(shí)現(xiàn)思路它不需要復(fù)雜算法不需要云端同步甚至不需要聯(lián)網(wǎng)。2. 在動(dòng)手寫代碼前先想清楚你要“騙”的是自己還是系統(tǒng)這類 App 最大的坑是我見過很多人第一版就做成了“強(qiáng)制鎖屏”或“限制使用時(shí)長”。結(jié)果是什么用戶會(huì)立刻想辦法繞過限制不但沒睡反而多花了二十分鐘折騰手機(jī)限制設(shè)置。所以我要先把邊界說清楚“騙自己睡覺”的邏輯不是“不允許你看手機(jī)”而是“讓看手機(jī)變得沒有吸引力”。它不是管控工具而是一個(gè)放松觸發(fā)器?;谶@個(gè)判斷我建議把功能重點(diǎn)放在“視覺降噪”和“流程引導(dǎo)”上而不是“權(quán)限控制”上。2.1 視覺降噪是第一步很多人睡前刷手機(jī)停不下來是因?yàn)槭謾C(jī)屏幕里信息密度太高。短視頻、彈幕、紅點(diǎn)、未讀消息每一個(gè)元素都在刺激大腦。在 App 設(shè)計(jì)上可以采用“深色低飽和界面”背景不使用純黑而是偏灰藍(lán)的暗色因?yàn)榧兒谠诎倒猸h(huán)境下對(duì)比過強(qiáng)反而刺眼文字減少亮色不要大面積使用白色正文按鈕盡量少每屏只保留一個(gè)主操作顏色上避免紅色、橙色這種高喚醒顏色改用低飽和的藍(lán)灰色或暗綠色。這里有一個(gè)容易忽略的細(xì)節(jié)睡前模式的文字大小要比正常模式更大。因?yàn)槿嗽诎胨胄褷顟B(tài)下眼睛對(duì)焦能力下降小字會(huì)讓人下意識(shí)瞇眼反而更清醒。字號(hào)大一些會(huì)減少閱讀壓力。2.2 用“引導(dǎo)流程”代替“強(qiáng)制彈窗”假設(shè)用戶設(shè)定晚上 23:00 準(zhǔn)備睡覺。常見的錯(cuò)誤做法是 23:00 彈一個(gè)全屏彈窗“你該睡覺了”。這種彈窗只會(huì)帶來焦慮。更自然的流程是22:40 開始提示“要不要準(zhǔn)備一下睡前流程”22:50 切換到一個(gè)極簡頁面只有一個(gè)開關(guān)或一個(gè)開始按鈕。23:00 進(jìn)入“睡眠模式”背景變成極暗色原本的資訊、數(shù)據(jù)、復(fù)雜界面全部隱藏。這種漸進(jìn)式引導(dǎo)的核心原因在于大腦從興奮狀態(tài)切換到睡眠狀態(tài)本身需要過渡。你突然讓大腦關(guān)機(jī)它反而會(huì)抵抗。2.3 自己騙自己也需要記錄反饋人很難靠意志力長期堅(jiān)持一件事但很容易因?yàn)榭吹揭粋€(gè)正向趨勢而堅(jiān)持。App 里可以記錄兩個(gè)數(shù)據(jù)準(zhǔn)備時(shí)間從按下“開始睡前準(zhǔn)備”到真正放下手機(jī)的時(shí)間入眠參考基于你設(shè)定的入睡時(shí)間和最后操作記錄生成一個(gè)“入睡偏移值”。這不是醫(yī)療級(jí)數(shù)據(jù)不測量心率不追蹤體動(dòng)它只是一個(gè)“自我觀察工具”。我在測試時(shí)發(fā)現(xiàn)這個(gè)記錄最有價(jià)值的不是“幾點(diǎn)睡”而是“從準(zhǔn)備到放下花了多久”。很多人以為自己睡前只玩了十分鐘手機(jī)實(shí)際記錄可能顯示“準(zhǔn)備了一個(gè)小時(shí)還沒關(guān)掉”這種反饋比鬧鐘更有效。3. 技術(shù)選型不追新框架用最小成本跑通本地應(yīng)用說到具體實(shí)現(xiàn)我不建議一開始就上 React Native、Flutter 或者小程序。理由很簡單你只是想騙自己睡覺不是想做一款上架的應(yīng)用商店產(chǎn)品。優(yōu)先考慮本地運(yùn)行、低依賴、易改動(dòng)的技術(shù)方案性價(jià)比更高。3.1 本地 Web App 是最快路徑如果你熟悉 HTML、CSS 和 JavaScript可以直接做一個(gè)本地頁面用瀏覽器打開使用。它有以下優(yōu)點(diǎn)不需要注冊(cè)開發(fā)者賬號(hào)不需要配置打包環(huán)境不需要處理多端兼容問題直接改代碼刷新頁面就能驗(yàn)證效果不依賴后端服務(wù)數(shù)據(jù)存在本地瀏覽器里。對(duì)個(gè)人工具來說“能跑、能改、能用”比“架構(gòu)標(biāo)準(zhǔn)、技術(shù)先進(jìn)”重要得多。3.2 本地化存儲(chǔ)選 localStorage 還是 IndexedDB這個(gè) App 需要存儲(chǔ)的數(shù)據(jù)很少無非是“設(shè)置時(shí)間”和“每日記錄”。用 localStorage 就足夠不需要引入數(shù)據(jù)庫。// 保存用戶設(shè)置 localStorage.setItem(sleep_start_hour, 23); localStorage.setItem(sleep_prepare_minutes, 20); // 讀取用戶設(shè)置 const hour localStorage.getItem(sleep_start_hour) || 23;localStorage 的好處是零配置、同步讀寫、夠穩(wěn)定。缺點(diǎn)是不能存大量結(jié)構(gòu)化數(shù)據(jù)。但在這個(gè)場景下根本用不到復(fù)雜查詢和關(guān)聯(lián)數(shù)據(jù)所以它反而最合適。當(dāng)然如果你想長期積累每天的數(shù)據(jù)并且希望后面做趨勢圖表localStorage 會(huì)越來越難維護(hù)。到時(shí)候可以切換為 IndexedDB。// 使用 IndexedDB 前先建庫 const request indexedDB.open(sleep-tracker, 1); request.onupgradeneeded function(event) { const db event.target.result; if (!db.objectStoreNames.contains(records)) { db.createObjectStore(records, { keyPath: date }); } };這里補(bǔ)充一個(gè)判斷標(biāo)準(zhǔn)當(dāng)單條記錄數(shù)量超過幾千條或者需要按日期范圍查詢時(shí)再考慮 IndexedDB如果只是每天一條記錄localStorage 完全夠用。3.3 移動(dòng)端適配優(yōu)先做成 PWA而不是獨(dú)立 App如果你希望它像真正的 App 一樣有圖標(biāo)、全屏、離線可用可以把它做成 PWA漸進(jìn)式 Web 應(yīng)用不需要上架應(yīng)用商店。PWA 需要準(zhǔn)備的核心文件一個(gè) manifest.json描述 App 名稱、圖標(biāo)、主題色一個(gè) Service Worker實(shí)現(xiàn)離線緩存一個(gè) HTTPS 訪問入口本地 localhost 在開發(fā)時(shí)不受此限制。{ name: 睡前準(zhǔn)備助手, short_name: 睡前助手, start_url: /, display: standalone, background_color: #101418, theme_color: #101418, icons: [ { src: /icon-192.png, sizes: 192x192, type: image/png } ] }Service Worker 的核心注冊(cè)邏輯不復(fù)雜if (serviceWorker in navigator) { window.addEventListener(load, function() { navigator.serviceWorker.register(/sw.js); }); }正是因?yàn)?PWA 既能離線運(yùn)行又能放在手機(jī)桌面還不需要應(yīng)用商店審核所以很適合這種個(gè)人工具類項(xiàng)目。3.4 是否要用藍(lán)牙、智能硬件、傳感器部分相關(guān)熱搜詞里提到了“藍(lán)牙 App 控制 ESP32”“智能硬件聯(lián)動(dòng)”加上不少睡眠類產(chǎn)品會(huì)配套燈光、音箱或手環(huán)。這里我給一個(gè)明確建議不要在第一版加入任何硬件依賴。原因有三個(gè)硬件增加排錯(cuò)成本藍(lán)牙連接失敗、設(shè)備掉電、協(xié)議不一致都會(huì)干擾“睡前放松”的體驗(yàn)傳感器數(shù)據(jù)未必準(zhǔn)確手機(jī)傳感器或手環(huán)的睡眠檢測本質(zhì)上只是推算用來做自我觀察夠用但不必為此增加復(fù)雜度個(gè)人項(xiàng)目維護(hù)成本高一旦硬件固件升級(jí)或接口變化App 也要跟著改違背了“最小可用”的初衷。如果你的目標(biāo)是學(xué)習(xí)硬件聯(lián)動(dòng)那自然可以單獨(dú)做一個(gè) ESP32 項(xiàng)目但如果目標(biāo)是“騙自己睡覺”先把軟件流程做穩(wěn)。4. 核心頁面設(shè)計(jì)從按下“準(zhǔn)備睡覺”到真正放下手機(jī)整個(gè) App 的頁面不必多核心頁控制在 3 個(gè)每一個(gè)頁面都有明確的職責(zé)。4.1 頁面一設(shè)置頁設(shè)置頁的作用是回答三個(gè)問題你希望幾點(diǎn)進(jìn)入睡前準(zhǔn)備你希望準(zhǔn)備階段持續(xù)多久你希望每天晚上幾點(diǎn)提醒自己設(shè)置項(xiàng)越少越好。我建議只保留三組參數(shù)參數(shù)說明推薦范圍入睡目標(biāo)時(shí)間你希望進(jìn)入睡眠模式的時(shí)間21:00 - 02:00準(zhǔn)備時(shí)長從第一次提醒到入睡準(zhǔn)備的過渡時(shí)間10 - 60 分鐘漸進(jìn)提醒開關(guān)是否分階段提醒準(zhǔn)備睡覺默認(rèn)開啟這里不要做“推送通知權(quán)限”“日歷同步”“天氣查詢”——每一項(xiàng)額外授權(quán)都會(huì)增加心理負(fù)擔(dān)。4.2 頁面二準(zhǔn)備過渡頁這是整個(gè) App 最關(guān)鍵的頁面。當(dāng)?shù)竭_(dá)準(zhǔn)備時(shí)間后頁面切換為極簡模式。這個(gè)頁面只做一件事顯示一個(gè)“我開始準(zhǔn)備睡覺”按鈕。點(diǎn)擊按鈕后進(jìn)入 20 分鐘倒計(jì)時(shí)。倒計(jì)時(shí)期間頁面上只顯示時(shí)間變化不顯示任何資訊、數(shù)據(jù)或鼓勵(lì)語。下面是一段簡化版實(shí)現(xiàn)邏輯!-- 極簡準(zhǔn)備頁 -- div idprepPage p idcountdown20:00/p button idstartSleepBtn我要睡覺了/button /divlet prepareMinutes 20; let timer null; document.getElementById(startSleepBtn).addEventListener(click, function() { // 關(guān)閉當(dāng)前頁面的高刺激元素 document.getElementById(prepPage).classList.add(low-stimulus); // 隱藏按鈕進(jìn)入倒計(jì)時(shí) this.style.display none; timer setInterval(() { prepareMinutes - 1; if (prepareMinutes 0) { clearInterval(timer); // 切換為全屏暗色睡眠模式 switchToSleepMode(); } else { document.getElementById(countdown).textContent formatTime(prepareMinutes); } }, 60000); });這段代碼只是骨架。實(shí)際使用時(shí)還要補(bǔ)充“暫停倒計(jì)時(shí)”“放棄倒計(jì)時(shí)”的邏輯因?yàn)槿瞬豢赡苊刻於寄芡昝缊?zhí)行流程如果點(diǎn)錯(cuò)按鈕也要有退出路徑。4.3 頁面三次日回顧頁第二天醒來打開 App可以看到一條簡單記錄昨晚準(zhǔn)備用時(shí)多少分鐘是否進(jìn)入了睡眠模式實(shí)際入睡時(shí)間和目標(biāo)時(shí)間的偏差。這個(gè)頁面不需要圖表不需要打分不需要分享。一行文字就夠“昨晚你從 23:10 開始準(zhǔn)備23:35 放下手機(jī)比目標(biāo)時(shí)間晚 10 分鐘?!敝圆唤ㄗh做“睡眠評(píng)分”是因?yàn)樵u(píng)分機(jī)制一旦存在就會(huì)給人壓力。睡覺這件事最怕“考核感”。5. 關(guān)鍵參數(shù)和交互細(xì)節(jié)讓“睡眠狀態(tài)”真正可感知App 的核心是設(shè)計(jì)體驗(yàn)參數(shù)細(xì)節(jié)比功能數(shù)量更影響效果。下面幾個(gè)點(diǎn)是我實(shí)測過程中反復(fù)調(diào)整過的。5.1 視覺狀態(tài)切換的漸變時(shí)間從正常界面切換到睡眠模式不要瞬間突變。瞬間的亮度變化會(huì)刺激視覺神經(jīng)。我調(diào)整到最舒服的值是正常亮度到睡眠亮度過渡時(shí)間 5 到 8 秒頁面背景色從#1A1D21漸變到#0B0D0F字體顏色從淺灰漸變到深灰漸變結(jié)束后頁面只保留一個(gè)“退出睡眠模式”的小按鈕。body { background-color: #0B0D0F; transition: background-color 6s ease; }這個(gè) 6 秒的過渡會(huì)讓大腦覺得環(huán)境在逐漸安靜下來而不是被強(qiáng)制關(guān)燈。5.2 提醒音與振動(dòng)提醒音不要用默認(rèn)的提示音那種短促高頻的“叮”聲在深夜容易讓人心慌。我建議使用低頻、長尾、音量漸強(qiáng)的聲音沒有音頻素材時(shí)可以先用系統(tǒng)振動(dòng)代替振動(dòng)模式選連續(xù)低幅振動(dòng)不要選急促間隔振動(dòng)。如果完全不想要聲音可以在設(shè)置里增加“完全靜音模式”只依賴屏幕亮度變化和文字提示。5.3 防止“退出后反而更清醒”的坑這是我在測試時(shí)發(fā)現(xiàn)的最關(guān)鍵問題。當(dāng)用戶主動(dòng)退出睡眠模式如果直接跳回正常 App 界面用戶會(huì)立刻看到完整的首頁、設(shè)置項(xiàng)和其他刺激內(nèi)容。這種“快速回到高刺激環(huán)境”反而會(huì)讓睡意消失。我建議做一層“最小化退出面板”退出睡眠模式后不回到首頁而是先進(jìn)入一個(gè)“我已經(jīng)醒了”的確認(rèn)頁確認(rèn)頁只有兩個(gè)選項(xiàng)“繼續(xù)睡覺”和“真正起床”“真正起床”被點(diǎn)擊后才允許回到正常界面。這個(gè)小改動(dòng)能大幅減少“睡前刷一下退出然后刷了半小時(shí)”的情況。6. 從單機(jī)版擴(kuò)展到可復(fù)用工具加一個(gè) API 或內(nèi)網(wǎng)訪問能力如果你不只是自己用還想讓家人或朋友在同一局域網(wǎng)內(nèi)使用可以考慮加一個(gè)簡單后端或本地接口。不過我要提醒的是這個(gè)需求如果沒有明確出現(xiàn)就不要在初版里做。原因很簡單加后端意味著加部署、加登錄、加數(shù)據(jù)同步、加安全策略一套組合拳下來原本的二十分鐘開發(fā)量會(huì)變成兩天。如果確實(shí)需要多設(shè)備使用更簡單的方案是使用局域網(wǎng)內(nèi)一臺(tái)主機(jī)提供靜態(tài)頁面訪問所有數(shù)據(jù)保存在各自瀏覽器的 localStorage 里不設(shè)置賬號(hào)體系不做跨設(shè)備同步。這種方案只適合“家里幾個(gè)人各自看自己的記錄”的場景但輕量、免維護(hù)。# 本地起一個(gè)靜態(tài)服務(wù)即可 python3 -m http.server 8080同一局域網(wǎng)內(nèi)的手機(jī)訪問電腦 IP 加端口就能打開頁面。6.1 是否要抓包或調(diào)試相關(guān)熱搜詞里出現(xiàn)了“App 抓包”“Fiddler 抓包手機(jī) App”“App 逆向”等內(nèi)容。如果你的目的是學(xué)習(xí)調(diào)試技巧那是另一條技術(shù)路線但在這個(gè)項(xiàng)目里我不建議引入抓包工具做常規(guī)使用。因?yàn)閭€(gè)人工具類應(yīng)用不存在復(fù)雜的接口鑒權(quán)或數(shù)據(jù)加密需求直接在瀏覽器開發(fā)者工具里看 Network 面板就足夠定位問題。6.2 網(wǎng)絡(luò)數(shù)據(jù)和遠(yuǎn)程依賴開發(fā)時(shí)盡量做到離線可用。為什么睡前場景可能出現(xiàn)在信號(hào)不好的臥室角落如果 App 啟動(dòng)時(shí)需要聯(lián)網(wǎng)請(qǐng)求數(shù)據(jù)一旦請(qǐng)求失敗用戶會(huì)看到加載失敗頁面反而增加焦慮本地優(yōu)先可以保證任何時(shí)候打開都能用。所有 JS 文件、CSS 文件、圖標(biāo)資源盡量本地存放不要依賴 CDN。如果你使用了外部字體或圖標(biāo)庫第一次加載成功后可以緩存但要保證離線時(shí)頁面主體功能不受影響。7. 實(shí)際運(yùn)行時(shí)序從設(shè)置到睡著的完整閉環(huán)把完整流程拉通一遍可以幫助你理解這個(gè) App 是怎么“騙”住你的。7.1 晚間準(zhǔn)備階段假設(shè)今天是第一天使用。晚上 22:40App 在后臺(tái)判斷時(shí)間到了準(zhǔn)備提醒點(diǎn)。頁面彈出第一層弱提示“今晚準(zhǔn)備幾點(diǎn)睡”你沒有理會(huì)繼續(xù)刷別的應(yīng)用。22:50App 再次提示這次頁面頂部出現(xiàn)“開始睡前準(zhǔn)備”按鈕。你點(diǎn)擊按鈕進(jìn)入準(zhǔn)備過渡頁此時(shí)頁面信息密度明顯降低。你看到 20 分鐘倒計(jì)時(shí)心里知道有一個(gè)“結(jié)束點(diǎn)”。到 23:10頁面自動(dòng)進(jìn)入純暗色睡眠模式。如果你在這個(gè)過程中想繼續(xù)刷手機(jī)也不會(huì)被強(qiáng)制阻止但每切回這個(gè) App看到的就是那個(gè)極暗的、沒有信息的頁面大腦會(huì)逐漸覺得“這里沒有可看的東西了”。7.2 第二天早晨階段第二天醒來手動(dòng)退出睡眠模式進(jìn)入回顧頁看到昨晚的記錄如果連續(xù)幾天實(shí)際入睡時(shí)間和目標(biāo)時(shí)間偏差變大App 會(huì)提示“最近入睡時(shí)間逐步后移可以把準(zhǔn)備時(shí)間提前 10 分鐘”。這個(gè)提示不建議做成彈窗放在頁面文字區(qū)域就好。const records loadRecords(); const recentDelay calculateAverageDelay(records, 7); if (recentDelay 15) { showSuggestion(最近入睡時(shí)間平均偏晚 15 分鐘試著把準(zhǔn)備開始時(shí)間提前 10 分鐘。); }這里不寫死算法邏輯很簡單取最近 7 天平均偏差超過閾值就給出建議。8. 常見報(bào)錯(cuò)和排查思路不慌先看日志個(gè)人項(xiàng)目調(diào)試時(shí)最容易出現(xiàn)問題的是三類頁面加載不出來定時(shí)器不觸發(fā)localStorage 讀取失敗。下面給一個(gè)通用排查順序。8.1 頁面加載不出來先看瀏覽器控制臺(tái)有沒有報(bào)錯(cuò)再看文件路徑是否正確尤其是本地打開時(shí)不要用file://協(xié)議直接跑 PWA如果使用了 Service Worker先禁用排除緩存問題確認(rèn)所有本地資源都存在沒有 404。注意本地調(diào)試時(shí)file://協(xié)議下部分瀏覽器功能會(huì)受限。優(yōu)先使用http://localhost方式啟動(dòng)。8.2 定時(shí)器不觸發(fā)最常見原因是頁面被瀏覽器放入后臺(tái)后定時(shí)器被節(jié)流。這是瀏覽器為了省電做的機(jī)制。解決方案第一次測試時(shí)不要讓頁面進(jìn)入后臺(tái)保持頁面可見真正需要后臺(tái)運(yùn)行時(shí)改用Notification或push方案但這會(huì)引入權(quán)限和服務(wù)器依賴如果只是個(gè)人使用就接受“頁面必須保持打開”這個(gè)限制。// 頁面可見性變化 document.addEventListener(visibilitychange, function() { if (document.visibilityState visible) { console.log(頁面重新可見檢查當(dāng)前狀態(tài)); } });8.3 localStorage 讀取失敗通常原因?yàn)g覽器禁用存儲(chǔ)域名不一致存儲(chǔ)內(nèi)容被其他代碼覆蓋。排查時(shí)先打印當(dāng)前值try { const value localStorage.getItem(sleep_start_hour); console.log(當(dāng)前入睡設(shè)置:, value); } catch (error) { console.error(讀取 localStorage 失敗:, error); }個(gè)人項(xiàng)目不需要很復(fù)雜的錯(cuò)誤處理但要在關(guān)鍵位置留下日志方便將來排查。9. 進(jìn)階方向把“睡眠記錄”做成一個(gè)可讀的數(shù)據(jù)報(bào)告當(dāng)你連續(xù)使用兩周之后你會(huì)發(fā)現(xiàn)原始記錄并不難收集真正有價(jià)值的是趨勢。這里不需要做復(fù)雜的機(jī)器學(xué)習(xí)簡單的統(tǒng)計(jì)就能說明問題。9.1 統(tǒng)計(jì)維度我建議統(tǒng)計(jì)三個(gè)維度指標(biāo)計(jì)算方式使用場景平均準(zhǔn)備時(shí)長總準(zhǔn)備時(shí)長 / 次數(shù)判斷自己是否越來越容易進(jìn)入狀態(tài)入睡偏移實(shí)際入睡時(shí)間 - 目標(biāo)時(shí)間判斷是否需要調(diào)整目標(biāo)連續(xù)達(dá)標(biāo)天數(shù)入睡偏差在 15 分鐘內(nèi)的連續(xù)天數(shù)給自己正向反饋9.2 展示方式展示不要用雷達(dá)圖、熱力圖、折線圖一起上。一個(gè)簡單的折線圖就夠。// 簡單統(tǒng)計(jì)示例 const records [ { date: 2025-01-01, prepareMinutes: 18, delay: 5 }, { date: 2025-01-02, prepareMinutes: 22, delay: 10 }, { date: 2025-01-03, prepareMinutes: 15, delay: -3 }, ]; const averagePrepare records.reduce((sum, r) sum r.prepareMinutes, 0) / records.length; console.log(平均準(zhǔn)備時(shí)長:, averagePrepare.toFixed(1), 分鐘);這些統(tǒng)計(jì)邏輯簡單到不需要引入圖表庫在頁面上用文字加一個(gè)簡單條形塊就能表達(dá)。9.3 數(shù)據(jù)導(dǎo)出如果你希望長期分析可以增加一個(gè)“導(dǎo)出 CSV”按鈕。function exportCSV(records) { const header 日期,準(zhǔn)備時(shí)長,入睡偏移\n; const rows records.map(r ${r.date},${r.prepareMinutes},${r.delay}).join(\n); const blob new Blob([header rows], { type: text/csv }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download sleep-records.csv; a.click(); URL.revokeObjectURL(url); }導(dǎo)出文件后你可以用 Excel 或任意數(shù)據(jù)分析工具做更多探索。這個(gè)功能總共不超過二十行代碼但價(jià)值很高。10. 最容易踩的 5 個(gè)坑實(shí)踐過程中我踩過不少坑。挑五個(gè)最典型的寫出來幫你避一避。10.1 坑一做得太多忘了核心是“睡前減負(fù)”第一版我總想加更多功能白噪音、冥想引導(dǎo)、呼吸訓(xùn)練、天氣提醒。結(jié)果每一項(xiàng)都會(huì)增加頁面內(nèi)容。后續(xù)我發(fā)現(xiàn)真正讓用戶放不下手機(jī)的是信息流不是功能缺失。這個(gè) App 的任務(wù)不是“提供更多”而是“提供更少”。10.2 坑二把“睡眠模式”做成“鎖機(jī)模式”如果用戶想關(guān)閉睡眠模式應(yīng)該能隨時(shí)關(guān)閉。強(qiáng)行鎖死不但在技術(shù)上容易引發(fā)麻煩在體驗(yàn)上也會(huì)產(chǎn)生對(duì)抗情緒。真正有效的方式是降低吸引力而不是切斷權(quán)限。10.3 坑三低估了視覺過渡的重要性直接切換亮暗會(huì)有明顯的突兀感。我測試時(shí)發(fā)現(xiàn)6 秒到 8 秒的漸變過渡可以讓體驗(yàn)自然很多。但過渡也不要超過 15 秒否則用戶會(huì)以為卡住了。10.4 坑四忘記處理“誤觸”和“意外退出”用戶可能在準(zhǔn)備階段突然來電話或者不小心退出頁面。如果沒有狀態(tài)記憶重新打開 App 后就要從頭設(shè)置時(shí)間。我建議把當(dāng)前狀態(tài)寫入 localStorage每次打開頁面時(shí)先恢復(fù)狀態(tài)。// 保存當(dāng)前狀態(tài) function saveState(state) { localStorage.setItem(sleep_app_state, JSON.stringify(state)); } // 恢復(fù)狀態(tài) function restoreState() { const raw localStorage.getItem(sleep_app_state); return raw ? JSON.parse(raw) : null; }10.5 坑五過于相信“數(shù)據(jù)完美”記錄數(shù)據(jù)的目的不是追求每一天都達(dá)標(biāo)而是觀察整體趨勢。如果某天沒記錄不要補(bǔ)錄不要美化空著就空著。一旦你開始“補(bǔ)數(shù)據(jù)”你觀察到的就不是自己而是你想成為的自己這個(gè)工具就失去意義了。11. 總結(jié)一下我認(rèn)為值得保留的設(shè)計(jì)原則最后留幾個(gè)原則你以后做類似個(gè)人工具類 App 時(shí)也能復(fù)用功能數(shù)量要克制每加一個(gè)功能都問一句“這會(huì)不會(huì)讓用戶看到更多信息”流程順序決定體驗(yàn)睡前這個(gè)場景所有交互都應(yīng)該朝“減少?zèng)Q策”方向設(shè)計(jì)。不追求權(quán)限最大化不需要的通知權(quán)限不要申請(qǐng)不需要的后臺(tái)任務(wù)不要啟動(dòng)。本地可用是底線個(gè)人工具最怕哪天服務(wù)關(guān)了、接口變了、CDN掛了應(yīng)用就不能用。記錄是為了觀察不是為了評(píng)分不要給用戶打分尤其是睡眠這種高度個(gè)人化的事情。如果你只是想做一個(gè)小工具自己用這個(gè)方案足夠簡單也不需要服務(wù)器、不需要數(shù)據(jù)庫、不需要開發(fā)者賬號(hào)。打開瀏覽器就能跑改起來也很快。如果你想把它做成一個(gè)真正的跨平臺(tái)應(yīng)用后續(xù)可以在這個(gè)基礎(chǔ)上引入 PWA 離線能力和本地打包方案但仍然要控制功能邊界。先跑通單機(jī)版用兩個(gè)星期記錄真實(shí)數(shù)據(jù)再?zèng)Q定要不要擴(kuò)展。這是讓我最受益的開發(fā)節(jié)奏。