Salvamento automático no editor de texto rico: salve no servidor sem perder edições

Adicione salvamento automático no servidor a um editor de texto rico em JavaScript sem enviar gravações simultâneas nem mostrar “Saved” antes da hora. Este guia fornece uma fila de salvamento testada, um contrato HTTP e uma lista de verificação para redes lentas e conflitos.
Um rascunho local no navegador é diferente de um salvamento no servidor. Para um app de escrita com armazenamento apenas local, use o guia de rascunhos em Markdown e download. Aqui, o servidor gerencia o documento e sua versão.
Defina primeiro o contrato de salvamento
Carregue um documento somente depois de verificar a autorização do usuário conectado. Retorne seu HTML e uma versão opaca em um cabeçalho de resposta ETag. A cada atualização, o cliente envia esse valor em If-Match. O servidor deve comparar a versão e atualizar tanto o HTML quanto a versão de forma atômica. Uma leitura seguida de uma gravação sem proteção não é suficiente.
- Retorne um novo
ETagsomente depois que a gravação for confirmada. - Retorne HTTP 412 para uma versão desatualizada; não sobrescreva o trabalho de outra aba.
- Valide o tamanho do corpo e as permissões do documento em cada requisição. Use a proteção CSRF da sua aplicação nas gravações autenticadas por cookies.
- Sanitize o HTML não confiável no servidor antes de renderizá-lo em outro lugar. Um editor não é o limite de segurança da sua aplicação.
Use uma requisição de salvamento por vez
Baixe o módulo de fila de salvamento para o seu app. Ele agrupa edições após uma pausa de 800ms, inicia um salvamento em até cinco segundos de digitação contínua quando não há uma requisição ativa e mantém o rascunho mais recente enquanto um salvamento está em andamento. Salvamentos lentos continuam sequenciais. Falhas mantêm o rascunho pendente; conflitos bloqueiam as gravações seguintes até que seu app os resolva.
A fila chama sua função save; ela não fornece servidor, autenticação nem mesclagem de edições colaborativas. Inicialize-a com o HTML que você realmente carregou, não com um conteúdo provisório.
Conecte o RichTextEditor ao seu endpoint
Comece pelo guia de início rápido do editor. O módulo a seguir pressupõe um editor inicializado, um documento existente em /api/documents/123 e controles de status e de nova tentativa na página. O endpoint abaixo é um contrato a implementar na linguagem do seu próprio servidor, não um serviço público do 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>
Torne as falhas visíveis e permita a recuperação
“Saved” significa que o HTML mais recente na fila foi confirmado, não apenas que uma requisição começou. Uma requisição malsucedida deixa o rascunho mais recente na memória. Ofereça uma nova tentativa para erros de rede e de servidor e uma ação de copiar ou baixar antes de recarregar após um conflito de versão. Não substitua automaticamente o conteúdo do editor por uma resposta de salvamento: isso pode mover o cursor e destruir o histórico de desfazer.
Este exemplo não oferece persistência offline. Fechar a aba ou uma falha do navegador pode causar a perda do trabalho não salvo. O aviso ao sair da página é uma tentativa sem garantia e não é exibido de forma confiável em dispositivos móveis. Não dependa de uma última requisição de rede durante pagehide. Adicione um rascunho de recuperação separado, local no navegador, se sua aplicação precisar dele, explique suas limitações de privacidade e nunca trate essa cópia como prova de um salvamento no servidor.
Salve imagens como URLs duráveis
Não persista marcadores temporários de upload como documentos concluídos. Coordene o salvamento automático com seu fluxo de upload e só marque o documento como totalmente salvo quando suas imagens tiverem URLs duráveis. Veja uploads de imagens em Node.js e como salvar texto rico com imagens.
Teste os cenários de falha
- Atrase um salvamento, digite A, depois B e depois C, e confirme que as requisições nunca se sobrepõem e que o documento salvo ao final é C.
- Retorne HTTP 500 uma vez. Confirme que o erro continua visível e que a nova tentativa envia o conteúdo mais recente.
- Abra a mesma versão em duas abas. Confirme que o segundo salvamento, com a versão desatualizada, retorna HTTP 412 e preserva as edições locais.
- Faça um salvamento ter sucesso sem um
ETag. Confirme que a interface não indica sucesso. - Teste documentos longos, uploads de imagens, recarregamentos, a ida do app para segundo plano em dispositivos móveis e sessões de login expiradas.
Continue com validação de formulários, a demonstração de salvamento e carregamento e as decisões de projeto do salvamento automático e suas implicações.