Automatisches Speichern im Rich-Text-Editor: Auf dem Server speichern, ohne Änderungen zu verlieren

Ergänzen Sie einen JavaScript-Rich-Text-Editor um automatisches Speichern auf dem Server, ohne überlappende Schreibzugriffe zu senden oder „Saved“ zu früh anzuzeigen. Diese Anleitung enthält eine getestete Speicherwarteschlange, einen HTTP-Vertrag und eine Checkliste für langsame Netzwerke und Konflikte.
Ein lokaler Entwurf im Browser ist etwas anderes als eine Speicherung auf dem Server. Für eine Schreib-App mit ausschließlich lokaler Speicherung verwenden Sie die Anleitung zu Markdown-Entwürfen und Downloads. Hier verwaltet der Server das Dokument und seine Version.
Definieren Sie zuerst den Speichervertrag
Laden Sie ein Dokument erst, nachdem Sie die Berechtigung des angemeldeten Benutzers geprüft haben. Geben Sie sein HTML und eine opake Versionskennung im Antwortheader ETag zurück. Bei jeder Aktualisierung sendet der Client diesen Wert in If-Match. Der Server muss die Version atomar vergleichen und sowohl HTML als auch Version aktualisieren. Ein Lesezugriff mit anschließendem ungeschütztem Schreibzugriff reicht nicht aus.
- Geben Sie ein neues
ETagerst zurück, nachdem der Schreibvorgang festgeschrieben wurde. - Geben Sie bei einer veralteten Version HTTP 412 zurück; überschreiben Sie nicht die Arbeit eines anderen Tabs.
- Prüfen Sie bei jeder Anfrage die Größe des Anfrageinhalts und die Dokumentberechtigungen. Verwenden Sie bei über Cookies authentifizierten Schreibzugriffen den CSRF-Schutz Ihrer Anwendung.
- Bereinigen Sie nicht vertrauenswürdiges HTML auf dem Server, bevor Sie es an anderer Stelle rendern. Ein Editor ist nicht die Sicherheitsgrenze Ihrer Anwendung.
Senden Sie jeweils nur eine Speicheranfrage
Laden Sie das Modul für die Speicherwarteschlange in Ihre App herunter. Es fasst Änderungen nach einer Pause von 800ms zusammen, startet bei fortlaufendem Tippen innerhalb von fünf Sekunden einen Speichervorgang, sofern keine Anfrage aktiv ist, und hält während eines laufenden Speichervorgangs den neuesten Entwurf vor. Auch langsame Speichervorgänge bleiben sequenziell. Bei Fehlern bleibt der Entwurf ausstehend; Konflikte blockieren weitere Schreibzugriffe, bis Ihre App sie auflöst.
Die Warteschlange ruft Ihre Funktion save auf; sie stellt weder einen Server noch Authentifizierung oder das Zusammenführen gemeinsam bearbeiteter Inhalte bereit. Initialisieren Sie sie mit dem tatsächlich geladenen HTML, nicht mit einem Platzhalter.
Verbinden Sie RichTextEditor mit Ihrem Endpunkt
Beginnen Sie mit dem Schnellstart für den Editor. Das folgende Modul setzt einen initialisierten editor, ein vorhandenes Dokument unter /api/documents/123 sowie eine Statusanzeige und ein Bedienelement zum Wiederholen auf der Seite voraus. Der folgende Endpunkt ist ein Vertrag, den Sie in Ihrer eigenen Serversprache implementieren müssen, kein öffentlicher RichTextEditor-Dienst.
<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>
Machen Sie Fehler sichtbar und behebbar
„Saved“ bedeutet, dass das neueste HTML in der Warteschlange bestätigt wurde, nicht bloß, dass eine Anfrage gestartet wurde. Bei einer fehlgeschlagenen Anfrage bleibt der neueste Entwurf im Arbeitsspeicher. Bieten Sie bei Netzwerk- und Serverfehlern eine Wiederholung an und nach einem Versionskonflikt eine Kopier- oder Downloadfunktion vor dem Neuladen. Ersetzen Sie den Editorinhalt nicht automatisch durch eine Speicherantwort: Das kann den Cursor verschieben und den Rückgängig-Verlauf zerstören.
Dieses Beispiel bietet keine dauerhafte Offline-Speicherung. Beim Schließen des Tabs oder einem Browserabsturz kann ungespeicherte Arbeit verloren gehen. Eine Warnung beim Verlassen der Seite wird nur nach Möglichkeit angezeigt und ist auf Mobilgeräten nicht zuverlässig. Verlassen Sie sich nicht auf eine letzte Netzwerkanfrage während pagehide. Ergänzen Sie bei Bedarf einen separaten lokalen Wiederherstellungsentwurf im Browser, erläutern Sie dessen Datenschutzgrenzen und betrachten Sie diese Kopie niemals als Nachweis einer Speicherung auf dem Server.
Speichern Sie Bilder als dauerhafte URLs
Speichern Sie vorübergehende Upload-Platzhalter nicht als fertige Dokumente. Stimmen Sie das automatische Speichern mit Ihrem Upload-Ablauf ab und markieren Sie das Dokument erst dann als vollständig gespeichert, wenn seine Bilder dauerhafte URLs haben. Siehe Bild-Uploads mit Node.js und Speichern von Rich Text mit Bildern.
Testen Sie die Fehlerfälle
- Verzögern Sie einen Speichervorgang, geben Sie A, dann B, dann C ein und prüfen Sie, dass sich Anfragen nie überlappen und das zuletzt gespeicherte Dokument C ist.
- Geben Sie einmal HTTP 500 zurück. Prüfen Sie, dass der Fehler sichtbar bleibt und die Wiederholung den neuesten Inhalt sendet.
- Öffnen Sie dieselbe Version in zwei Tabs. Prüfen Sie, dass der zweite, veraltete Speicherversuch HTTP 412 zurückgibt und lokale Änderungen erhalten bleiben.
- Lassen Sie einen Speichervorgang ohne
ETagerfolgreich sein. Prüfen Sie, dass die Benutzeroberfläche keinen Erfolg meldet. - Testen Sie lange Dokumente, Bild-Uploads, Neuladen, den Wechsel in den Hintergrund auf Mobilgeräten und abgelaufene Anmeldesitzungen.
Fahren Sie mit Formularvalidierung, der Demo zum Speichern und Laden und Entwurfsabwägungen beim automatischen Speichern fort.