您现在的位置是:网站首页 > 不备份数据(“数据库挂了再说”)文章详情
不备份数据(“数据库挂了再说”)
陈川
【
前端综合
】
57091人已围观
5571字
“数据库挂了再说”,这种心态在开发中并不少见,尤其是前端工程师可能觉得数据备份是后端的责任。但现实是,前端同样需要关注数据持久化、缓存策略和灾难恢复方案,否则用户操作丢失、白屏报错等问题会直接暴露给用户。
为什么前端也需要考虑数据备份
前端不直接管理数据库,但用户的操作数据(如表单输入、本地配置)可能因网络问题、浏览器崩溃或代码错误而丢失。例如:
- 用户填写了30分钟的复杂表单,提交时网络中断
- 本地存储的
localStorage
被意外清空 - 单页应用(SPA)的路由状态因刷新而重置
// 典型的数据丢失场景
const unsavedData = { /* 用户输入的复杂数据 */ };
fetch('/api/save', {
method: 'POST',
body: JSON.stringify(unsavedData)
}).catch(() => {
// 网络错误后数据彻底消失
});
前端数据备份的常见方案
自动草稿保存
通过debounce
或throttle
定期保存数据到IndexedDB
或服务器草稿箱:
import { debounce } from 'lodash';
const saveDraft = debounce(async (data) => {
try {
await indexedDB.put('drafts', data);
// 或调用草稿API
} catch (e) { console.error('备份失败', e); }
}, 3000);
// 监听表单变化
form.addEventListener('input', () => saveDraft(formData));
离线优先策略
使用Service Worker缓存关键请求,配合Cache API
实现离线访问:
// service-worker.js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((cached) => cached || fetch(event.request))
);
});
操作日志追溯
记录用户操作序列,出现问题时可以回放:
const actionLog = [];
function logAction(action, payload) {
actionLog.push({ timestamp: Date.now(), action, payload });
// 可限制日志长度避免内存泄漏
}
// 示例:记录表格排序操作
logAction('SORT_TABLE', { column: 'name', direction: 'asc' });
灾难恢复的兜底方案
错误边界与数据恢复组件
React错误边界中可以尝试恢复数据:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
// 尝试从备份恢复
const backupData = localStorage.getItem('crashBackup');
if (backupData) this.setState({ recoveryData: JSON.parse(backupData) });
}
render() {
if (this.state.hasError) {
return <RecoveryUI data={this.state.recoveryData} />;
}
return this.props.children;
}
}
心跳检测与异常上报
定期检查服务可用性,提前发现问题:
function heartbeat() {
return fetch('/health')
.then(res => {
if (!res.ok) throw new Error('服务异常');
localStorage.setItem('lastHealthy', Date.now());
})
.catch(() => {
// 触发降级方案
showWarning('连接不稳定,已启用离线模式');
});
}
setInterval(heartbeat, 30000);
用户感知层面的设计
明确的状态提示
通过UI明确告知用户数据状态:
<div class="status-bar">
<span id="save-status">已保存</span>
<span id="last-sync" title="上次同步时间">2分钟前</span>
</div>
冲突解决界面
当本地修改与服务器版本冲突时:
function handleConflict(localData, serverData) {
return new Promise((resolve) => {
renderModal(`
<h3>数据冲突</h3>
<p>本地版本:${localData.updatedAt}</p>
<p>服务器版本:${serverData.updatedAt}</p>
<button onclick="resolve(localData)">保留我的更改</button>
<button onclick="resolve(serverData)">使用服务器版本</button>
`);
});
}
性能与体验的平衡
增量备份策略
对大体积数据采用差异备份:
let lastBackup = null;
function incrementalBackup(currentData) {
const changes = deepDiff(lastBackup, currentData);
if (changes) {
saveToBackend(changes);
lastBackup = cloneDeep(currentData);
}
}
备份频率的权衡
根据数据类型调整备份频率:
数据类型 | 备份频率 | 存储位置 |
---|---|---|
表单草稿 | 每5秒 | IndexedDB |
用户偏好 | 每次变更 | localStorage |
临时计算数据 | 不备份 | 内存 |
浏览器存储方案的局限性
localStorage的陷阱
localStorage
有同步阻塞特性,大容量写入会导致页面卡顿:
// 错误示范:一次性保存大对象
localStorage.setItem('bigData', JSON.stringify(largeObj)); // 可能阻塞主线程
// 改进方案:改用IndexedDB
const db = await idb.openDB('backup', 1);
await db.put('backups', largeObj, 'user123');
隐私模式下的应对
隐私模式下某些API不可用,需要降级处理:
function getBackupStorage() {
try {
localStorage.setItem('test', '1');
localStorage.removeItem('test');
return localStorage;
} catch {
return { // 内存降级方案
_data: new Map(),
setItem: (k,v) => this._data.set(k,v),
getItem: (k) => this._data.get(k)
};
}
}
监控与预警机制
前端错误日志收集
通过window.onerror
捕获未处理的异常:
window.onerror = (msg, url, line, col, error) => {
navigator.sendBeacon('/log', {
type: 'unhandled_error',
stack: error?.stack,
state: getCurrentAppState() // 记录出错前的状态
});
};
存储空间监控
定期检查剩余存储空间:
async function checkStorageQuota() {
if (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate();
if (usage / quota > 0.9) {
alert('存储空间不足,请清理历史数据');
}
}
}
开发流程中的预防措施
自动化测试覆盖
编写备份恢复的测试用例:
describe('数据备份', () => {
beforeEach(() => mockIndexedDB());
it('崩溃后应从本地恢复数据', async () => {
await saveBackup({ id: 1, content: '测试' });
crashSimulator();
const recovered = await loadBackup();
expect(recovered).toEqual({ id: 1, content: '测试' });
});
});
Code Review检查清单
在CR时重点关注:
- 是否有未处理的Promise拒绝
- 敏感操作是否缺少确认对话框
- 关键数据变更是否有备份逻辑
当灾难真的发生时
用户引导流程
设计清晰的数据恢复指引:
function renderRecoveryPage() {
return `
<div class="recovery">
<h2>数据恢复</h2>
<p>检测到上次会话异常结束</p>
<button onclick="checkLocalBackup()">尝试恢复本地备份</button>
<button onclick="fetchServerBackup()">从服务器恢复</button>
</div>
`;
}
数据差异对比工具
帮助用户选择要恢复的版本:
function diffViewer(a, b) {
return htm`
<div class="diff">
${Object.keys(a).map(key => htm`
<div class="row">
<div class="old">${a[key]}</div>
<div class="new">${b[key]}</div>
</div>
`)}
</div>
`;
}
下一篇: 使用冷门技术栈(用 Elm 写业务逻辑)