先產(chǎn)品仍需要備份出口)
把筆記放在瀏覽器里很適合一個(gè)不想強(qiáng)迫用戶注冊(cè)的小產(chǎn)品打開(kāi)即寫(xiě)離線可讀也少了一套賬號(hào)服務(wù)。可是“本地優(yōu)先”容易被誤說(shuō)成“永不丟失”。瀏覽器緩存、用戶清理、隱私窗口、設(shè)備損壞都不會(huì)因?yàn)楫a(chǎn)品小而繞開(kāi)它。保存狀態(tài)與備份狀態(tài)分開(kāi)顯示MDN 的存儲(chǔ)與清理說(shuō)明區(qū)分默認(rèn)的 best-effort 存儲(chǔ)與持久化申請(qǐng)。申請(qǐng)成功也不阻止用戶主動(dòng)刪除數(shù)據(jù)。界面上可以顯示“已保存到此瀏覽器”不該直接寫(xiě)“永久保存”。另一個(gè)明確動(dòng)作應(yīng)該是“導(dǎo)出備份”。我會(huì)讓第一版產(chǎn)品保存一份帶版本號(hào)的 JSON并提供導(dǎo)入。版本號(hào)讓未來(lái)遷移有落腳點(diǎn)字段檢查則避免把損壞文件或其他產(chǎn)品的數(shù)據(jù)當(dāng)成自己的筆記。導(dǎo)入前先預(yù)覽數(shù)量不悄悄覆蓋現(xiàn)有記錄。同步、加密和備份是三件事不能靠一個(gè)云朵圖標(biāo)混在一起。賬號(hào)并不是默認(rèn)答案Chrome 在 10 月 5 日介紹的 Email Verification API 變更仍涉及 origin trial 和版本差異。對(duì)于一個(gè)小型離線工具這類(lèi)登錄能力可以關(guān)注卻不適合當(dāng)作所有瀏覽器已經(jīng)具備的基礎(chǔ)設(shè)施。先把數(shù)據(jù)帶得走往往比先加登錄更能降低用戶的遷移成本。下面的代碼通過(guò)注入 storage 接口演示保存與載入。Node 中用內(nèi)存對(duì)象做替身瀏覽器使用時(shí)才替換為localStorage并在調(diào)用處捕獲配額、權(quán)限等異常。測(cè)試替身能驗(yàn)證序列化與校驗(yàn)流程不能證明瀏覽器不會(huì)清理數(shù)據(jù)。執(zhí)行node notes.js驗(yàn)證保存后重載、壞版本拒絕和重復(fù) ID 拒絕。本例沒(méi)有跨設(shè)備同步?jīng)]有測(cè) IndexedDB也沒(méi)有將備份文件寫(xiě)到云端。小產(chǎn)品不必一開(kāi)始就解決所有災(zāi)難但必須給用戶一個(gè)清楚、可以親手拿走的出口??瑟?dú)立運(yùn)行的核心例子function decode(raw) { const xJSON.parse(raw); if(x.version!1||!Array.isArray(x.notes)||x.notes.length1000) throw Error(不支持的備份格式); const idsnew Set(); for(const n of x.notes) { if(typeof n.id!string||!n.id||ids.has(n.id)|| typeof n.text!string||n.text.length20000) throw Error(筆記字段無(wú)效); ids.add(n.id); } return x; } function repository(storage) { return { save(notes) {const rawJSON.stringify({version:1,notes});decode(raw);storage.setItem(notes-v1,raw);return raw;}, load() {const rawstorage.getItem(notes-v1);return raw?decode(raw).notes:[];} }; } const memorynew Map(); const storage{setItem:(k,v)memory.set(k,v),getItem:kmemory.get(k)??null}; const reporepository(storage); const backuprepo.save([{id:a,text:給下一次創(chuàng)作留一頁(yè)白紙}]); if(repository(storage).load()[0].id!a)throw Error(重載失敗); for(const bad of [{version:2,notes:[]}, {version:1,notes:[{id:a,text:1},{id:a,text:2}]}]) { let rejectedfalse;try{decode(bad);}catch{rejectedtrue;} if(!rejected)throw Error(壞備份應(yīng)被拒絕); } console.log(backup);