您现在的位置是:网站首页 > 不备份数据(“数据库挂了再说”)文章详情

不备份数据(“数据库挂了再说”)

“数据库挂了再说”,这种心态在开发中并不少见,尤其是前端工程师可能觉得数据备份是后端的责任。但现实是,前端同样需要关注数据持久化、缓存策略和灾难恢复方案,否则用户操作丢失、白屏报错等问题会直接暴露给用户。

为什么前端也需要考虑数据备份

前端不直接管理数据库,但用户的操作数据(如表单输入、本地配置)可能因网络问题、浏览器崩溃或代码错误而丢失。例如:

  • 用户填写了30分钟的复杂表单,提交时网络中断
  • 本地存储的localStorage被意外清空
  • 单页应用(SPA)的路由状态因刷新而重置
// 典型的数据丢失场景
const unsavedData = { /* 用户输入的复杂数据 */ };
fetch('/api/save', {
  method: 'POST',
  body: JSON.stringify(unsavedData)
}).catch(() => {
  // 网络错误后数据彻底消失
});

前端数据备份的常见方案

自动草稿保存

通过debouncethrottle定期保存数据到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时重点关注:

  1. 是否有未处理的Promise拒绝
  2. 敏感操作是否缺少确认对话框
  3. 关键数据变更是否有备份逻辑

当灾难真的发生时

用户引导流程

设计清晰的数据恢复指引:

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>
  `;
}

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步