读取完整源码
保存按钮、导出或提交时调用 getMarkdown()。读取结果是 Markdown 文本,保留编辑器支持的原始文件格式策略。
const markdown = editor.getMarkdown()
// 将 markdown 交给应用自己的持久化接口。
示例中的「读取 Markdown」只生成当前快照。之后继续编辑时,需要再次读取才能得到最新文本。
订阅增量
onChange 提供 documentId、revision、changes、selection 和 origin。可以标记未保存状态,或交给支持增量的宿主存储;无需为渲染工具栏而在每次输入时读取全文。
const unsubscribe = editor.onChange(event => {
console.log('文档版本', event.documentId, event.revision)
})
// 观察者结束生命周期时取消订阅。
unsubscribe()
自动保存时,由应用安排保存频率并处理失败、重试和提交顺序。不要把服务端保存响应直接当成需要再次替换全文的输入。
外部更新
applyChanges() 接收预期的文档身份与基础版本。偏移必须属于该版本,旧版本会被拒绝;失败后重新取得最新状态并处理冲突。
const result = editor.applyChanges({
documentId: editor.documentId,
baseRevision: editor.revision,
changes: { from: 0, to: 0, insert: '新增说明。\n\n' },
origin: 'host',
})
if (!result.ok) console.warn(result.reason)
版本化修改接口本身不构成多人协同协议。切换到另一份文档时,使用 replaceDocument(markdown, documentId)。
可直接接入的顺序自动保存
下面的 TypeScript 模块把同一份文档的保存请求串行化,延迟读取快照;源码继续变化时,当前请求完成后再保存新版本。错误后保留 dirty 状态,下一次编辑或显式 flush() 重试。它不把响应写回编辑器。
import type { AutosaveOptions, AutosaveSession, SaveSnapshot } from "./types";
/** @param options - Host persistence policy and one current document. @returns Serial save ownership for that document only. */
export function attachAutosave(options: AutosaveOptions): AutosaveSession {
const { editor } = options;
const scope = editor.documentScope.fork();
let dirty = false;
let timer: ReturnType<typeof setTimeout> | undefined;
let running: Promise<void> | undefined;
/** Writes successive immutable snapshots; slow responses never replace the open source. */
async function drain(): Promise<void> {
while (dirty && !scope.disposed) {
dirty = false;
const snapshot: SaveSnapshot = { documentId: editor.documentId, revision: editor.revision, markdown: editor.getMarkdown() };
options.status("saving");
try {
await options.save(snapshot, scope.signal);
if (scope.disposed) return;
if (!dirty && editor.revision === snapshot.revision) options.status("saved");
} catch (error) {
if (!scope.disposed) { dirty = true; options.status("error"); options.error(error); }
return; // An explicit flush or the next source edit retries; no busy retry loop.
}
}
}
/** @returns Completion of prior work and any source dirty when this flush can acquire the writer. */
async function flush(): Promise<void> {
clearTimeout(timer);
// Recheck after awaiting: another flush may have acquired the single writer.
while (running) await running;
if (scope.disposed || !dirty) return;
running = drain().finally(() => { running = undefined; });
await running;
}
scope.own(editor.onChange(() => {
dirty = true;
options.status("dirty");
clearTimeout(timer);
timer = setTimeout(() => { void flush(); }, options.delay ?? 600);
}));
scope.own(() => clearTimeout(timer));
return { flush, dispose: () => scope.dispose() };
}
import { attachAutosave } from './recipes/autosave'
const saving = attachAutosave({
editor,
save: async (snapshot, signal) => {
const response = await fetch(`/api/documents/${encodeURIComponent(snapshot.documentId)}`, {
method: 'PUT', signal, headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
body: snapshot.markdown,
})
if (!response.ok) throw new Error(`save-http-${response.status}`)
},
status: value => { console.log('保存状态', value) },
error: error => { console.error(error) },
})
// 页面离开前先等待 flush,并由应用根据保存状态决定是否允许离开。
await saving.flush()
/api/documents/:id 需要应用提供。这个 adapter 从首次源码变化开始保存,不会自动把刚加载的文档标为 dirty。flush() 结束不代表一定成功:失败由 status/error 报告,应用必须保留错误状态和重试入口。
服务器并发和离线
本地 revision 是编辑器内计数,不是服务器版本。多客户端写入时,服务端还需要 ETag、版本号或协同协议;遇到 409/412 由应用进行合并或冲突选择,不能无限重试覆盖他人的保存。
离线缓存属于宿主存储层,可缓存精确 Markdown 和服务端版本。没有 durable acknowledgement 前,不应显示“已保存到服务器”。不要在 beforeunload 中假设可以完成一个任意异步请求。
换文档与外部回声
顺序是:处理当前未保存状态、结束旧保存 session、replaceDocument()、给新文档创建新 session。旧 documentScope 会取消仍在处理的资源。
同文档外部修改用 applyChanges(),偏移属于 baseRevision 的逻辑文本。成功时得到新的 revision;origin 可标记外部来源,宿主可在 onChange 里抑制同源回声。发生 stale-target 时,不要只把 baseRevision 改成最新值后重复旧 changes,必须重新映射或合并。
格式和存储职责
编辑器接收解码后的字符串。文件编码、BOM 的文件字节处理、文件名、权限、附件存储和下载都由应用负责。需要纯文本快照时用 getMarkdown();需要编辑偏移时用逻辑 state.doc。不要把渲染 HTML 或组件状态当作可恢复的 Markdown 存档。