把笔记放在浏览器里,很适合一个不想强迫用户注册的小产品:打开即写,离线可读,也少了一套账号服务。可是“本地优先”容易被误说成“永不丢失”。浏览器缓存、用户清理、隐私窗口、设备损坏,都不会因为产品小而绕开它。
保存状态与备份状态分开显示
MDN 的存储与清理说明区分默认的 best-effort 存储与持久化申请。申请成功也不阻止用户主动删除数据。界面上可以显示“已保存到此浏览器”,不该直接写“永久保存”。另一个明确动作应该是“导出备份”。
我会让第一版产品保存一份带版本号的 JSON,并提供导入。版本号让未来迁移有落脚点;字段检查则避免把损坏文件或其他产品的数据当成自己的笔记。导入前先预览数量,不悄悄覆盖现有记录。同步、加密和备份是三件事,不能靠一个云朵图标混在一起。
账号并不是默认答案
Chrome 在 10 月 5 日介绍的 Email Verification API 变更仍涉及 origin trial 和版本差异。对于一个小型离线工具,这类登录能力可以关注,却不适合当作所有浏览器已经具备的基础设施。先把数据带得走,往往比先加登录更能降低用户的迁移成本。
下面的代码通过注入 storage 接口演示保存与载入。Node 中用内存对象做替身,浏览器使用时才替换为localStorage,并在调用处捕获配额、权限等异常。测试替身能验证序列化与校验流程,不能证明浏览器不会清理数据。
执行node notes.js验证保存后重载、坏版本拒绝和重复 ID 拒绝。本例没有跨设备同步,没有测 IndexedDB,也没有将备份文件写到云端。小产品不必一开始就解决所有灾难,但必须给用户一个清楚、可以亲手拿走的出口。
可独立运行的核心例子
function decode(raw) { const x=JSON.parse(raw); if(x.version!==1||!Array.isArray(x.notes)||x.notes.length>1000) throw Error('不支持的备份格式'); const ids=new Set(); for(const n of x.notes) { if(typeof n.id!=='string'||!n.id||ids.has(n.id)|| typeof n.text!=='string'||n.text.length>20000) throw Error('笔记字段无效'); ids.add(n.id); } return x; } function repository(storage) { return { save(notes) {const raw=JSON.stringify({version:1,notes});decode(raw);storage.setItem('notes-v1',raw);return raw;}, load() {const raw=storage.getItem('notes-v1');return raw?decode(raw).notes:[];} }; } const memory=new Map(); const storage={setItem:(k,v)=>memory.set(k,v),getItem:k=>memory.get(k)??null}; const repo=repository(storage); const backup=repo.save([{id:'a',text:'给下一次创作留一页白纸'}]); 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 rejected=false;try{decode(bad);}catch{rejected=true;} if(!rejected)throw Error('坏备份应被拒绝'); } console.log(backup);