个人站点上的“本地优先”功能(书签、阅读进度、主题偏好、成就)几乎都住在 localStorage 里。它没有数据库那样的迁移工具,但同样需要版本策略——否则老访客的旧数据会在新版本里制造无法复现的 bug。
一、统一命名:前缀 + 命名空间 + 版本
一个站点的所有 key 应当有共同前缀,冒号分层,末尾带 schema 版本:
myblog:reading-achievements:v1
myblog:accent-theme:v1
myblog:bookmark:<slug>
对比反面教材:同一站点里混用 myblog-bookmark-favorites(连字符、无版本)和 myblog:recent-searches:v1。前者难 grep、难批量清理、无法判断格式版本。统一命名看似小事,却决定了“一键清除本站数据”“跨标签同步”“旧数据迁移”这些能力能不能廉价实现。
二、迁移而非破坏:读时归一化
不要在用户下次访问时直接丢弃或崩溃于旧格式。最稳的是读时迁移:
const NEW_KEY = 'myblog:bookmark-favorites:v1';
const OLD_KEY = 'myblog-bookmark-favorites';
function readFavorites() {
try {
// 旧 key 存在且新 key 未写 → 一次性搬迁,再删旧 key
if (!localStorage.getItem(NEW_KEY) && localStorage.getItem(OLD_KEY)) {
localStorage.setItem(NEW_KEY, localStorage.getItem(OLD_KEY));
localStorage.removeItem(OLD_KEY);
}
const raw = localStorage.getItem(NEW_KEY) || '[]';
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return []; // 隐私模式/配额/坏数据:降级为空,绝不抛穿到 UI
}
}
要点:
- 迁移是幂等的(新 key 已存在就跳过),重复执行安全;
- 解析失败一律 catch 到默认值——
localStorage在隐私模式可能抛异常,坏 JSON 可能来自半次写入; - 结构升级(v1→v2)用同样套路:读 v1,转换,写 v2,保留 v1 一个版本周期以便回滚。
三、写操作要有超时与配额意识
localStorage.setItem 在超出配额(多数浏览器约 5–10MB)时抛 QuotaExceededError。任何“保存”都应包在 try/catch 里,失败时至少静默降级、最好给用户可见提示。把持久化视为 best-effort,功能正确性不能依赖它成功。
四、必须提供“清除我的数据”入口
本地存储对用户不可见,一个诚实的站点应提供一键清除。实现很直接——遍历删除带统一前缀的 key:
function wipeAll(prefix = 'myblog') {
const doomed = [];
for (let i = 0; i < localStorage.length; i += 1) {
const k = localStorage.key(i);
if (k && k.startsWith(prefix)) doomed.push(k);
}
doomed.forEach((k) => localStorage.removeItem(k));
}
统一前缀在这里显出价值:没有它,你无法安全地“只清本站数据”。配合二次确认弹窗,构成对个人数据的最低尊重。
五、跨标签页一致性
localStorage 改动会触发其他同源标签页的 storage 事件。主题、偏好类状态应监听它,保证多标签不各说各话:
window.addEventListener('storage', (e) => {
if (e.key === 'myblog:accent-theme:v1') applyAccent(e.newValue);
});
注意 storage 事件不在改动的那个标签页自身触发,别把它当本标签页的更新通知用。
小结
localStorage 的“本地优先”是隐私与离线体验的红利,但也把数据治理责任从服务端搬到了浏览器。一条命名规范、一次读时迁移、一个清除入口、一个跨标签同步,就构成了个人站本地存储最小的可持续性方案——它保证三年后你改了这个存储结构,老访客回来时看到的是无缝升级,而不是丢失的书签和一个“只有我能复现”的 bug。