Classic 不会自动把正文写进你的数据库。应用负责保存位置、文档身份和未保存提示;产品提供当前内容及已安装的格式服务。本页从已挂载的 instance 开始,创建方式见快速接入。
1. 记录真正的正文变化
产品插件的 onUpdate 还会响应选区和界面刷新,不适合直接当作“正文已修改”。在插件的 onMount 订阅当前 Core 的 update,比较前后文档身份,在 onUnmount 解除订阅。结构更新重建 Core 后会重新绑定;语言、只读和内置主题更新保留原订阅。
import type { QuireEditor, QuireEditorUpdateEvent } from "@quirekit/core";
import { defineClassicEditorPlugin } from "@quirekit/classic-editor";
import type { ClassicEditorPlugin } from "@quirekit/classic-editor";
/**
* Observes document changes across product remounts without treating UI updates as edits.
* @param onChange - Host callback that marks the current document as unsaved.
* @returns Product plugin; create a separate plugin object for each Classic instance.
*/
export function createClassicDraftObserver(onChange: () => void): ClassicEditorPlugin {
let owner: QuireEditor | undefined;
/** @param event - Accepted update, which can also represent an editability change. */
function onUpdate(event: QuireEditorUpdateEvent): void {
// Core already suppresses equal-content updates; an editability notification
// retains the same immutable document. The observer needs only identity.
if (event.previousState.doc !== event.editor.state.doc) onChange();
}
/** Releases the subscription belonging to the previous mounted Core. */
function detach(): void {
owner?.off("update", onUpdate);
owner = undefined;
}
return defineClassicEditorPlugin({
id: "app.draft-observer",
/** @param context - Current mount, whose Core owns this subscription. */
onMount(context) {
detach();
owner = context.editor;
owner?.on("update", onUpdate);
},
onUnmount: detach,
onDestroy: detach,
});
}
创建时把 createClassicDraftObserver(() => { dirty = true }) 加入实例的 plugins。dirty 是应用自己的状态;一个插件对象只交给一个实例。例程不在每次变化时序列化全文,切换只读也不会误报正文修改。显式使用 emitUpdate: false 的恢复流程应由宿主建立新的已保存基线。
2. 保存一个确定的版本
instance.getJSON() 读取当前规范内容。备份还应带上自己的版本、模型和文档 ID;文档名称可通过 instance.setDocumentName(name) 更新,名称由应用另行持久化,不包含在 getJSON() 中。
下面的 Word 草稿例程先捕获内容,再等待存储确认。返回 true 才表示当前页面仍停留在刚保存的版本;返回 false 表示这次写入完成了,但期间发生了编辑、重挂载或退出,不应清除未保存提示。
import type { ClassicDraftOwner, ClassicDraftRepository } from "./types";
/**
* Saves one revision without clearing changes made while storage was pending.
* @param instance - Mounted product that owns the current document.
* @param repository - Application storage; it must serialize writes for the same document.
* @returns Whether the acknowledged revision still belongs to the active Core and document.
*/
export async function saveClassicDraft(
instance: ClassicDraftOwner,
repository: ClassicDraftRepository
): Promise<boolean> {
const editor = instance.editor;
if (instance.isDestroyed || !editor || editor.isDestroyed) {
throw new Error("Mount the document before saving");
}
// Capture before awaiting: a later edit or product remount owns a different revision.
const document = editor.state.doc;
const source = JSON.stringify({ version: 1, model: "word-v1", content: instance.getJSON() });
await repository.write(source);
return !instance.isDestroyed && !editor.isDestroyed
&& instance.editor === editor && editor.state.doc === document;
}
repository.write(source) 由你连接接口或本地存储,失败应拒绝 Promise。保存按钮在请求期间禁用,或由 repository 串行写同一文档;返回值只保护界面的保存状态,不能阻止服务端并发请求倒序覆盖。多用户编辑还需要版本冲突协议。这个例程只保存 JSON,图片字节仍需持久媒体服务。
3. 打开 JSON 备份
先读取原文件,检查模型、版本、节点、标记、属性和 Schema,再提交。完整校验顺序见内容校验与文件操作。损坏文件不能替换当前草稿。
内置 json 服务读取原生规范文档 JSON,并通过相同的提交边界严格替换;它不识别本页例程带有版本、模型和 content 包装的业务备份。业务备份先由应用解包校验,再按下面的流程打开。
打开另一份业务文档时,先处理当前未保存修改,再建立新会话边界:
instance.update({ document: validatedContent })
instance.setDocumentName('项目说明')
这会销毁旧 Core 并用新正文挂载;原撤销历史不会保留。插件通过挂载钩子重新订阅。应用同步切换文档 ID、存储键和已保存基线,并取消旧文档的保存任务。不要在输入事件中反复传入 document。
只恢复当前会话正文时,可以使用 Core 的 replaceDocument(),它连同顶层属性替换且本次不计入历史;这与创建新会话的生命周期不同。
4. 装配 DOCX / DOTX 服务
createClassicWordDocxPlugin() 默认注册 docx、dotx 两个服务。它们与 Word preset 配合,通过 plugins 安装一次。预设本身不代表这些服务已经注册。
plugins: [createClassicWordDocxPlugin({
onReport(report) {
showConversionReport(report.warnings, report.diagnostics)
},
})]
showConversionReport 是你的界面函数:区分解析警告与内容近似、保留或丢失的诊断。导入成功不代表完全无损;不符合 Schema 的块可能被降级为纯文本并报告 schema-invalid-block。不要把一次成功返回显示成“格式完全保留”。配置见 DOCX 插件参考。
5. 调用文件服务
await instance.importDocument('docx', {
data: new Uint8Array(await file.arrayBuffer()),
fileName: file.name,
mimeType: file.type,
})
const artifact = await instance.exportDocument('docx')
file 是宿主选择的 File。DOCX 需要字节,不能传文本;DOTX 使用 dotx 服务 ID。导出返回 data、fileName、mimeType,宿主负责下载或上传,成功生成文件不等于数据库已保存。服务不存在、没有对应操作或产品未挂载时,调用会拒绝 Promise;转换失败也需要捕获并展示。
6. 取消与防止迟到覆盖
importDocument() 为每次调用捕获当前 Core 与正文版本。新的导入、正文修改(包括修改后撤销)、只读配置更新、卸载或销毁,会让尚未提交的旧导入失效。移动光标不使正文版本过期。官方 DOCX 服务在最后的同步提交边界复核这些条件,过期时拒绝为 AbortError,保留当前正文与名称,也不发成功转换报告。
const opening = new AbortController()
try {
const data = new Uint8Array(await file.arrayBuffer())
await instance.importDocument('docx', {
data, fileName: file.name, mimeType: file.type,
}, { signal: opening.signal })
} catch (error) {
if (!(error instanceof DOMException && error.name === 'AbortError')) throw error
}
// 用户取消、选择另一文件或退出时,由宿主调用 opening.abort()
在读取文件字节前建立 controller;选择新文件时先取消上一次,防止旧文件的 arrayBuffer() 迟到后才启动导入。Abort 可以及时结束调用方等待,但底层不支持取消的 codec 仍可能完成内部解析;最终提交仍被拦截。已经提交的写入不能通过随后 abort 撤销。
只读会在导入入口与提交前各检查一次,应用还需检查业务授权。若要打开完全不同的业务文档,仍需处理未保存修改、存储键和会话边界;导入不是文档管理系统。
7. 自定义 import 的迁移
自定义服务先解析和校验,再在 context.commit(() => editor.commands.replaceDocument(...)) 中执行一次同步命令,并在提交后报告成功。不要把 async 回调传给 commit,也不要在 commit 外直接写正文。旧插件若绕过这个合同直接调用命令,运行时无法回滚其任意副作用;应升级插件后再依赖过期保护。完整上下文见 Classic API。
导出是否捕获点击时快照由服务实现决定;官方 DOCX 服务会捕获开始时的正文。导入图片的暂存与失败清理见持久媒体。自己开发 Core 产品时,仍使用解析、校验、提交的宿主流程。
实例和配置的完整字段见Classic 实例与配置参考。