Enregistrement automatique dans un éditeur de texte enrichi : enregistrer sur votre serveur sans perdre de modifications

RichTextEditor avec sa barre d’outils complète et un document modifiable

Ajoutez l’enregistrement automatique sur le serveur à un éditeur de texte enrichi JavaScript sans envoyer d’écritures simultanées ni afficher « Saved » trop tôt. Ce guide fournit une file d’enregistrement testée, un contrat HTTP et une liste de vérification pour les réseaux lents et les conflits.

Un brouillon local dans le navigateur est différent d’un enregistrement sur le serveur. Pour une application de rédaction entièrement locale, utilisez le guide des brouillons Markdown et du téléchargement. Ici, le serveur gère le document et sa version.

Définissez d’abord le contrat d’enregistrement

Ne chargez un document qu’après avoir vérifié les autorisations de l’utilisateur connecté. Renvoyez son HTML et une version opaque dans un en-tête de réponse ETag. À chaque mise à jour, le client envoie cette valeur dans If-Match. Le serveur doit comparer la version et mettre à jour le HTML et la version de manière atomique. Une lecture suivie d’une écriture sans protection ne suffit pas.

  • Ne renvoyez un nouvel ETag qu’après la validation définitive de l’écriture.
  • Renvoyez HTTP 412 pour une version périmée ; n’écrasez pas le travail effectué dans un autre onglet.
  • Vérifiez la taille du corps et les autorisations sur le document à chaque requête. Utilisez la protection CSRF de votre application pour les écritures authentifiées par cookie.
  • Assainissez le HTML non fiable sur le serveur avant de l’afficher ailleurs. Un éditeur ne constitue pas la frontière de sécurité de votre application.

N’envoyez qu’une requête d’enregistrement à la fois

Téléchargez le module de file d’enregistrement dans votre application. Il regroupe les modifications après une pause de 800ms, lance un enregistrement dans les cinq secondes de saisie continue lorsqu’aucune requête n’est active et conserve le brouillon le plus récent pendant un enregistrement. Les enregistrements lents restent séquentiels. En cas d’échec, le brouillon reste en attente ; les conflits bloquent les écritures suivantes jusqu’à ce que votre application les résolve.

La file appelle votre fonction save ; elle ne fournit ni serveur, ni authentification, ni fusion collaborative. Initialisez-la avec le HTML réellement chargé, et non avec un contenu provisoire.

Connectez RichTextEditor à votre point de terminaison

Commencez par le guide de démarrage rapide de l’éditeur. Le module suivant suppose qu’un éditeur editor est initialisé, qu’un document existe à l’adresse /api/documents/123 et que la page dispose de commandes d’état et de nouvelle tentative. Le point de terminaison ci-dessous est un contrat à implémenter dans votre propre langage serveur, et non un service public 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>

Rendez les échecs visibles et permettez la récupération

« Saved » signifie que le dernier HTML mis en file a été acquitté, et pas simplement qu’une requête a démarré. Une requête qui échoue laisse le brouillon le plus récent en mémoire. Proposez une nouvelle tentative pour les erreurs réseau et serveur, ainsi qu’une action de copie ou de téléchargement avant de recharger la page après un conflit de version. Ne remplacez pas automatiquement le contenu de l’éditeur par une réponse d’enregistrement : cela peut déplacer le curseur et détruire l’historique d’annulation.

Cet exemple ne fournit pas de persistance hors ligne. Fermer l’onglet ou subir un plantage du navigateur peut entraîner la perte du travail non enregistré. Un avertissement de navigation est une mesure sans garantie et ne s’affiche pas de manière fiable sur mobile. Ne comptez pas sur une dernière requête réseau lors de pagehide. Ajoutez un brouillon de récupération local distinct dans le navigateur si votre application en a besoin, expliquez ses limites en matière de confidentialité et ne considérez jamais cette copie comme la preuve d’un enregistrement sur le serveur.

Enregistrez les images avec des URL durables

N’enregistrez pas comme documents terminés des contenus provisoires liés à des envois d’images en cours. Coordonnez l’enregistrement automatique avec votre processus d’envoi et ne marquez le document comme entièrement enregistré que lorsque ses images possèdent des URL durables. Consultez les envois d’images avec Node.js et l’enregistrement de texte enrichi avec des images.

Testez les cas d’échec

  1. Retardez un enregistrement, saisissez A puis B puis C, et vérifiez que les requêtes ne se chevauchent jamais et que le document finalement enregistré est C.
  2. Renvoyez HTTP 500 une fois. Vérifiez que l’erreur reste visible et qu’une nouvelle tentative envoie le contenu le plus récent.
  3. Ouvrez la même version dans deux onglets. Vérifiez que le second enregistrement, fondé sur une version périmée, renvoie HTTP 412 et conserve les modifications locales.
  4. Faites réussir un enregistrement sans ETag. Vérifiez que l’interface n’annonce pas de réussite.
  5. Testez les documents longs, les envois d’images, les rechargements, le passage en arrière-plan sur mobile et les sessions de connexion expirées.

Poursuivez avec la validation des formulaires, la démonstration d’enregistrement et de chargement et les compromis de conception de l’enregistrement automatique.

À lire ensuite