从崩溃中拯救文档:Quill编辑器自动保存完全指南

从崩溃中拯救文档:Quill编辑器自动保存完全指南

【免费下载链接】quill Quill is a modern WYSIWYG editor built for compatibility and extensibility 【免费下载链接】quill 项目地址: https://gitcode.com/gh_mirrors/qui/quill

你是否经历过这样的噩梦?花了半小时精心编辑的文档,因为浏览器崩溃、网络中断或意外关闭标签页而瞬间消失。这种数据丢失不仅浪费时间,更会严重打击用户的创作热情。作为一款现代富文本编辑器,Quill提供了强大的自动保存能力,但许多开发者并未充分利用其潜力。本文将带你深入了解如何基于Quill的History模块实现完善的自动保存功能,从根本上解决文档丢失问题。

了解Quill的History模块

Quill的自动保存功能建立在其核心的History模块之上,该模块位于packages/quill/src/modules/history.ts。这个模块不仅处理撤销/重做操作,还记录了文档的所有变更历史,是实现自动保存的基础。

History模块的核心原理是维护两个栈结构:undo栈和redo栈。每当用户进行编辑操作时,系统会创建一个Delta对象来描述此次变更,并将其压入undo栈。默认配置下,History模块有三个关键参数:

  • delay: 1000 - 变更记录的延迟时间(毫秒),避免过于频繁的记录
  • maxStack: 100 - 最大历史记录数量,防止内存占用过大
  • userOnly: false - 是否只记录用户触发的变更
static DEFAULTS: HistoryOptions = {
  delay: 1000,
  maxStack: 100,
  userOnly: false,
};

实现自动保存的三种方案

基于Quill的架构,我们可以通过三种方式实现自动保存功能,每种方案各有适用场景。

1. 基于History API的轻量级保存

最简单的实现方式是直接利用History模块已有的记录。通过监听text-change事件,我们可以在用户编辑时触发保存逻辑。这种方式的优点是实现简单,无需额外记录变更。

// 初始化Quill编辑器
const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    history: {
      delay: 2000,  // 延长延迟时间,减少保存频率
      maxStack: 200 // 增加历史记录数量
    }
  }
});

// 监听文本变化事件
let saveTimeout;
quill.on('text-change', function(delta, oldContents, source) {
  // 忽略程序触发的变更,只处理用户操作
  if (source !== 'user') return;
  
  // 使用防抖避免频繁保存
  clearTimeout(saveTimeout);
  saveTimeout = setTimeout(() => {
    // 获取当前文档内容
    const content = quill.getContents();
    // 保存到本地存储
    localStorage.setItem('quill-auto-save', JSON.stringify(content));
    console.log('文档已自动保存');
  }, 3000); // 3秒无操作后保存
});

2. 完整文档定期保存

对于重要文档,建议定期保存完整的文档内容。Quill的Editor类提供了getContents()方法(位于packages/quill/src/core/editor.ts第158-160行),可以获取整个文档的Delta表示。

getContents(index: number, length: number): Delta {
  return this.delta.slice(index, index + length);
}

利用这个方法,我们可以实现定时自动保存功能:

// 定期保存函数
function autoSave() {
  // 获取完整文档内容
  const content = quill.getContents();
  // 获取当前选区位置,用于恢复
  const selection = quill.getSelection();
  
  // 构建保存对象
  const saveData = {
    content: content,
    selection: selection,
    timestamp: new Date().toISOString()
  };
  
  // 可以选择保存到localStorage或发送到服务器
  localStorage.setItem('full-document-save', JSON.stringify(saveData));
  
  // 更新保存状态指示器
  updateSaveIndicator(true);
}

// 设置定时保存,每60秒一次
const saveInterval = setInterval(autoSave, 60000);

// 也可以在窗口关闭前强制保存
window.addEventListener('beforeunload', function(e) {
  autoSave();
  // 标准做法是不显示提示,但可以确保保存完成
  e.returnValue = '文档正在保存...';
});

3. 基于Delta差异的增量保存

对于大型文档或协作编辑场景,增量保存是更高效的方式。我们可以只保存变更的Delta,而不是整个文档。History模块的record方法(位于packages/quill/src/modules/history.ts第111-136行)展示了如何记录变更:

record(changeDelta: Delta, oldDelta: Delta) {
  if (changeDelta.ops.length === 0) return;
  this.stack.redo = [];
  let undoDelta = changeDelta.invert(oldDelta);
  let undoRange = this.currentRange;
  const timestamp = Date.now();
  if (
    this.lastRecorded + this.options.delay > timestamp &&
    this.stack.undo.length > 0
  ) {
    const item = this.stack.undo.pop();
    if (item) {
      undoDelta = undoDelta.compose(item.delta);
      undoRange = item.range;
    }
  } else {
    this.lastRecorded = timestamp;
  }
  if (undoDelta.length() === 0) return;
  this.stack.undo.push({ delta: undoDelta, range: undoRange });
  if (this.stack.undo.length > this.options.maxStack) {
    this.stack.undo.shift();
  }
}

基于这一原理,我们可以实现增量保存:

// 存储初始文档状态
let lastSavedDelta = quill.getContents();

// 监听文本变化事件
quill.on('text-change', function(delta, oldContents, source) {
  if (source !== 'user') return;
  
  // 计算与上次保存的差异
  const changeDelta = lastSavedDelta.diff(quill.getContents());
  
  // 如果有实际变更,保存差异
  if (changeDelta.ops.length > 0) {
    // 发送差异到服务器或保存到本地
    saveDeltaChange(changeDelta);
    
    // 更新最后保存的状态
    lastSavedDelta = quill.getContents();
  }
});

// 保存差异的函数
function saveDeltaChange(delta) {
  const saveData = {
    delta: delta,
    timestamp: new Date().getTime()
  };
  
  // 可以将多个小的变更合并后再保存
  // 这里简化处理,直接保存每次变更
  const changes = JSON.parse(localStorage.getItem('delta-changes') || '[]');
  changes.push(saveData);
  
  // 只保留最近的100次变更
  if (changes.length > 100) {
    changes.shift();
  }
  
  localStorage.setItem('delta-changes', JSON.stringify(changes));
}

实现文档恢复功能

自动保存的最终目的是为了在需要时能够恢复文档。以下是一个完整的恢复功能实现:

// 恢复文档函数
function restoreDocument() {
  // 尝试从localStorage获取保存的数据
  const savedData = localStorage.getItem('full-document-save');
  
  if (savedData) {
    try {
      const { content, selection, timestamp } = JSON.parse(savedData);
      
      // 将保存的内容应用到编辑器
      quill.setContents(content);
      
      // 恢复光标位置
      if (selection) {
        quill.setSelection(selection.index, selection.length);
      }
      
      // 显示恢复提示
      showNotification(`已恢复上次保存的文档 (${new Date(timestamp).toLocaleString()})`);
    } catch (e) {
      console.error('恢复文档失败:', e);
      showNotification('恢复文档失败,可能是数据已损坏', 'error');
    }
  }
}

// 页面加载时检查是否有可恢复的文档
window.addEventListener('load', function() {
  // 询问用户是否恢复
  if (localStorage.getItem('full-document-save') && confirm('检测到未保存的文档,是否恢复?')) {
    restoreDocument();
  }
});

构建可视化保存状态

为了提升用户体验,我们应该提供清晰的保存状态反馈。可以在编辑器界面添加一个状态指示器,让用户随时了解文档的保存情况。

<div id="editor-container">
  <div id="save-status" class="save-status">
    <span class="status-icon">⏳</span>
    <span class="status-text">未保存</span>
  </div>
  <div id="editor"></div>
</div>
.save-status {
  position: absolute;
  top: 10px;
  right: 10px;
  padding: 5px 10px;
  border-radius: 4px;
  font-size: 12px;
  background-color: #f5f5f5;
  display: flex;
  align-items: center;
  gap: 5px;
}

.status-saved {
  background-color: #e8f5e9;
  color: #2e7d32;
}

.status-saving {
  background-color: #fff3e0;
  color: #f57c00;
}

.status-error {
  background-color: #ffebee;
  color: #c62828;
}
// 更新保存状态指示器
function updateSaveIndicator(status) {
  const statusEl = document.getElementById('save-status');
  const iconEl = statusEl.querySelector('.status-icon');
  const textEl = statusEl.querySelector('.status-text');
  
  statusEl.className = 'save-status';
  
  if (status === true) {
    statusEl.classList.add('status-saved');
    iconEl.textContent = '✓';
    textEl.textContent = '已保存';
  } else if (status === 'saving') {
    statusEl.classList.add('status-saving');
    iconEl.textContent = '⏳';
    textEl.textContent = '保存中...';
  } else if (status === 'error') {
    statusEl.classList.add('status-error');
    iconEl.textContent = '⚠️';
    textEl.textContent = '保存失败';
  } else {
    textEl.textContent = '未保存';
  }
}

高级优化:冲突解决与版本管理

对于多设备同步或协作编辑场景,我们需要处理潜在的冲突问题。可以实现一个简单的版本管理系统:

class VersionManager {
  constructor(editor) {
    this.editor = editor;
    this.versions = JSON.parse(localStorage.getItem('document-versions') || '[]');
    this.maxVersions = 10; // 最多保留10个版本
  }
  
  // 创建新版本
  createVersion(description) {
    const content = this.editor.getContents();
    const selection = this.editor.getSelection();
    const version = {
      id: Date.now(),
      timestamp: new Date().toISOString(),
      description: description || '自动保存',
      content: content,
      selection: selection
    };
    
    // 添加新版本并限制数量
    this.versions.unshift(version);
    if (this.versions.length > this.maxVersions) {
      this.versions.pop();
    }
    
    // 保存到localStorage
    localStorage.setItem('document-versions', JSON.stringify(this.versions));
    return version;
  }
  
  // 获取所有版本
  getVersions() {
    return [...this.versions];
  }
  
  // 恢复指定版本
  restoreVersion(versionId) {
    const version = this.versions.find(v => v.id === versionId);
    if (version) {
      this.editor.setContents(version.content);
      if (version.selection) {
        this.editor.setSelection(version.selection.index, version.selection.length);
      }
      return true;
    }
    return false;
  }
}

// 使用版本管理器
const versionManager = new VersionManager(quill);

// 每天创建一个版本点
setInterval(() => {
  versionManager.createVersion('定时版本备份');
}, 86400000);

// 也可以在关键操作前创建版本
document.getElementById('publish-btn').addEventListener('click', function() {
  versionManager.createVersion('发布前备份');
  // 发布逻辑...
});

最佳实践与性能优化

实现自动保存时,需要注意以下几点以确保功能可靠性和性能:

  1. 合理设置保存频率:过于频繁的保存会影响性能,建议设置2-5秒的防抖延迟
  2. 区分保存目标:小的变更可以保存在localStorage,完整备份应考虑IndexedDB或服务器
  3. 数据压缩:对于大型文档,可以使用JSON压缩库减小保存数据的体积
  4. 错误处理:保存和恢复过程都需要完善的错误处理,避免程序崩溃
  5. 用户控制:提供手动保存按钮和禁用自动保存的选项,尊重用户选择
// 使用pako库压缩大型文档
import pako from 'pako';

function compressData(data) {
  const jsonStr = JSON.stringify(data);
  const compressed = pako.deflate(jsonStr, { level: 6 });
  return btoa(String.fromCharCode.apply(null, compressed));
}

function decompressData(compressedData) {
  const binaryString = atob(compressedData);
  const compressed = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) {
    compressed[i] = binaryString.charCodeAt(i);
  }
  const jsonStr = pako.inflate(compressed, { to: 'string' });
  return JSON.parse(jsonStr);
}

总结

通过本文介绍的方法,你可以基于Quill编辑器构建可靠的自动保存系统。从简单的本地存储到复杂的版本管理,Quill的History模块和Delta格式提供了灵活的基础。选择适合你应用场景的方案,并根据用户反馈不断优化,将极大提升编辑器的用户体验,让用户不再担心文档丢失的问题。

最后,不要忘记Quill的官方文档中关于History模块的详细说明,以及社区提供的各种自动保存插件,可以帮助你进一步完善这一功能。

完整的自动保存实现代码可以在项目的test/e2e/fixtures/目录下找到更多示例,这些测试用例展示了Quill如何处理各种编辑场景下的变更记录。

【免费下载链接】quill Quill is a modern WYSIWYG editor built for compatibility and extensibility 【免费下载链接】quill 项目地址: https://gitcode.com/gh_mirrors/qui/quill

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值