Guardado automático en un editor de texto enriquecido: guarda en tu servidor sin perder ediciones

RichTextEditor con su barra de herramientas completa y un documento editable

Añade guardado automático en el servidor a un editor de texto enriquecido JavaScript sin enviar escrituras simultáneas ni mostrar “Saved” demasiado pronto. Esta guía incluye una cola de guardado probada, un contrato HTTP y una lista de comprobación para redes lentas y conflictos.

Un borrador local del navegador es distinto de un guardado en el servidor. Para una aplicación de escritura que funcione solo de forma local, usa la guía de borradores Markdown y descargas. Aquí el servidor gestiona el documento y su versión.

Define primero el contrato de guardado

Carga un documento solo después de comprobar la autorización del usuario que inició sesión. Devuelve su HTML y una versión opaca en un encabezado de respuesta ETag. En cada actualización, el cliente envía ese valor en If-Match. El servidor debe comparar la versión y actualizar tanto el HTML como la versión de forma atómica. Una lectura seguida de una escritura sin protección no es suficiente.

  • Devuelve un nuevo ETag solo después de que la escritura se confirme de forma definitiva.
  • Devuelve HTTP 412 para una versión desactualizada; no sobrescribas el trabajo de otra pestaña.
  • Valida el tamaño del cuerpo y los permisos del documento en cada solicitud. Usa la protección CSRF de tu aplicación para las escrituras autenticadas mediante cookies.
  • Sanea el HTML no confiable en el servidor antes de renderizarlo en otro lugar. Un editor no constituye el límite de seguridad de tu aplicación.

Envía una sola solicitud de guardado a la vez

Descarga el módulo de la cola de guardado en tu aplicación. Agrupa las ediciones tras una pausa de 800ms, inicia un guardado en un plazo de cinco segundos de escritura continua cuando no hay ninguna solicitud activa y conserva el borrador más reciente mientras hay un guardado en curso. Los guardados lentos siguen siendo secuenciales. Los fallos mantienen el borrador pendiente; los conflictos bloquean las escrituras posteriores hasta que tu aplicación los resuelva.

La cola llama a tu función save; no proporciona un servidor, autenticación ni fusión colaborativa. Inicialízala con el HTML que realmente cargaste, no con contenido provisional.

Conecta RichTextEditor a tu endpoint

Empieza con la guía de inicio rápido del editor. El siguiente módulo supone que tienes un editor inicializado, un documento existente en /api/documents/123 y controles de estado y reintento en la página. El endpoint siguiente es un contrato que debes implementar en tu propio lenguaje de servidor, no un servicio público de RichTextEditor.

<p id="save-status" role="status" aria-live="polite"></p>
<button id="retry-save" type="button">Retry save</button>
<script type="module">
import { createDraftSaver } from "/js/draft-autosave.mjs";
// Your initialized RichTextEditor instance:
const editor = window.editor;
const status = document.getElementById("save-status");
const retry = document.getElementById("retry-save");
const endpoint = "/api/documents/123";
const loaded = await fetch(endpoint, { credentials: "same-origin" });
if (!loaded.ok) throw new Error("Document could not be loaded");
let version = loaded.headers.get("ETag");
if (!version) throw new Error("Server must return an ETag");
const documentData = await loaded.json();
editor.setHTMLCode(documentData.html);
const messages = {
  unsaved: "Unsaved changes", saving: "Saving…", saved: "Saved",
  error: "Not saved. Keep this tab open and retry.",
  conflict: "Another version exists. Copy your work before reloading."
};
const saver = createDraftSaver({
  initialHTML: editor.getHTMLCode(),
  onStatus(state) {
    status.textContent = messages[state];
    retry.disabled = state !== "error";
  },
  async save(html) {
    const response = await fetch(endpoint, {
      method: "PUT", credentials: "same-origin",
      headers: { "Content-Type": "application/json", "If-Match": version },
      body: JSON.stringify({ html })
      // Add your application's CSRF token/header here.
    });
    if (response.status === 412 || response.status === 409) {
      throw Object.assign(new Error("Version conflict"), { code: "CONFLICT" });
    }
    if (!response.ok) throw new Error("Save failed: " + response.status);
    const nextVersion = response.headers.get("ETag");
    if (!nextVersion) throw new Error("Save response is missing its ETag");
    version = nextVersion;
  }
});
retry.disabled = true;
editor.attachEvent("change", () => saver.schedule(editor.getHTMLCode()));
retry.addEventListener("click", () => { void saver.flush().catch(() => {}); });
window.addEventListener("beforeunload", event => {
  if (saver.pending) { event.preventDefault(); event.returnValue = ""; }
});
</script>

Haz visibles los fallos y permite recuperarse de ellos

“Saved” significa que se confirmó la recepción del último HTML de la cola, no solo que se inició una solicitud. Una solicitud fallida deja el borrador más reciente en memoria. Ofrece reintentos para los errores de red y del servidor, y una acción para copiar o descargar antes de recargar tras un conflicto de versión. No reemplaces automáticamente el contenido del editor con una respuesta de guardado: eso puede mover el cursor y destruir el historial de deshacer.

Este ejemplo no proporciona persistencia sin conexión. Cerrar la pestaña o un fallo del navegador pueden provocar la pérdida del trabajo sin guardar. Una advertencia de navegación es una medida sin garantías y no se muestra de forma fiable en dispositivos móviles. No dependas de una última solicitud de red durante pagehide. Añade un borrador de recuperación local e independiente en el navegador si tu aplicación lo necesita, explica sus límites de privacidad y nunca trates esa copia como prueba de un guardado en el servidor.

Guarda las imágenes con URL duraderas

No guardes los marcadores provisionales de cargas en curso como documentos terminados. Coordina el guardado automático con tu flujo de carga y marca el documento como completamente guardado solo cuando sus imágenes tengan URL duraderas. Consulta las cargas de imágenes en Node.js y cómo guardar texto enriquecido con imágenes.

Prueba los casos de fallo

  1. Retrasa un guardado, escribe A, luego B y luego C, y confirma que las solicitudes nunca se superpongan y que el documento final guardado sea C.
  2. Devuelve HTTP 500 una vez. Confirma que el error siga visible y que el reintento envíe el contenido más reciente.
  3. Abre la misma versión en dos pestañas. Confirma que el segundo guardado, con una versión desactualizada, devuelva HTTP 412 y conserve las ediciones locales.
  4. Haz que un guardado se complete correctamente sin un ETag. Confirma que la interfaz no indique que se guardó correctamente.
  5. Prueba documentos largos, cargas de imágenes, recargas, el paso a segundo plano en dispositivos móviles y sesiones de inicio de sesión caducadas.

Continúa con la validación de formularios, la demostración de guardado y carga y las decisiones y los compromisos de diseño del guardado automático.

Para seguir leyendo