Skip to content

指南 · 基础

版本基线 1.5.0。本篇把「会加载保存」用到「懂模型」:mode 三态、documentBuffer 哨兵状态、修订追踪(tracked changes)、批注、ref 方法,以及「canonical OOXML、语义无损往返」到底意味着什么。

一、三种 mode:editing / suggesting / viewing

EditorMode = 'editing' | 'suggesting' | 'viewing'(默认 editing):

mode行为
editing正常编辑,改动直接写入文档
suggesting修订模式:每次编辑变成一条修订(插入下划线、删除删除线),带作者与时间
viewing带工具栏的只读查看
tsx
const [mode, setMode] = useState<EditorMode>('editing');
<DocxEditor documentBuffer={buf} mode={mode} onModeChange={setMode} />;

onModeChange 表示你自己持有 mode 状态;不传则编辑器内部管理(用户可在工具栏的模式选择器切换)。

mode 与 readOnly 是两回事

readOnly 禁用一切输入手段(打字、工具栏、对话框)但保留完整分页渲染mode 控制「编辑还是记修订还是只看」。想「能读但仍可加建议」用 mode="suggesting",想「纯查看器」用 readOnly + showToolbar={false}。两者相互独立。

二、documentBuffer 的三种哨兵状态

documentBuffer 接受 DocxInputArrayBuffer/Uint8Array/Blob/File),另有三种值含义不同:

tsx
<DocxEditor documentBuffer={buf} />        // 挂载该文档
<DocxEditor documentBuffer={null} />       // 立即挂一个空文档(从零录入)
<DocxEditor documentBuffer={undefined} />  // 推迟挂载,避免 fetch 在途的空状态闪烁

若已持有解析好的 Document 树(来自 @eigenpal/docx-editor-core),用 document prop 传入即可跳过解析器。

三、修订追踪:写进 .docx 的是真 OOXML

suggesting 模式下,每次编辑都成为修订而非直接改动,并序列化为 Word 原生的 <w:ins> / <w:del>,因此「编辑器里审阅的文档」与「Word 里审阅的文档」可互换。

tsx
<DocxEditor documentBuffer={buf} author="Jess Lin" mode="suggesting" />

被追踪的不止行内文本,还包括:

  • 文本插入/删除/替换(显示为「Replaced X with Y」)
  • 段落结构:段落断点、段落属性变化
  • 表格:行/单元格的增删合并、行/单元格/表格属性变化
  • 图片:插入/删除
  • 列表:编号变化(拒绝时同时回退文本与编号)

这些都作为真实 OOXML 修订往返,不是编辑器私有状态。每条修订记录创建时间戳;author 与日期都能经保存、重载后保留。

接受/拒绝修订(API)

侧栏按钮背后是 @eigenpal/docx-editor-core/prosemirror/commands 的 ProseMirror 命令:

tsx
import { acceptAllChanges } from '@eigenpal/docx-editor-core/prosemirror/commands';

// 用 onEditorViewReady 捕获 view,再对 view 运行命令
<DocxEditor documentBuffer={buf} onEditorViewReady={(v) => (viewRef.current = v)} />;
// ...
const view = viewRef.current;
if (view) acceptAllChanges()(view.state, view.dispatch);

还有 acceptChangeById(id) / rejectChangeById(id) / rejectAllChanges()。枚举修订用 extractTrackedChanges(state)

四、用代码提一条修订:proposeChange

DocxEditorRef.proposeChange 不靠用户打字,按段落的 w14:paraId 锚定插入一条 tracked 替换(找不到段落或搜索文本时返回 false):

ts
ref.current?.proposeChange({
  paraId: 'ABC12300',
  search: 'thirty (30) days',
  replaceWith: 'sixty (60) days',
  author: 'Contract Bot',
});

这是「AI 红线(redlining)」构建于其上的原语。

五、回调:onChange / onSave / onError

tsx
<DocxEditor
  documentBuffer={buf}
  onChange={(doc) => {/* 每次文档变更,参数是解析后的 Document */}}
  onSave={(out) => {/* 用户经工具栏 Ctrl/Cmd+S 触发保存,out 是 ArrayBuffer */}}
  onError={(err) => report(err) /* 解析/渲染错误从这里抛出 */}
/>

区别:ref.save() 是你主动要字节(自动保存、自定义保存按钮);onSave 是用户通过内置 UI 触发保存时的回调。

六、「无损往返」到底是什么意思

.docx 是一堆 XML 部件的 ZIP,Word 还会写很多编辑器无须建模的 XML(书签、自定义 XML、邮件合并域、兼容性设置、VBA 工程……)。docx-editor 的管线是 解析 → 文档模型 → 编辑 → 序列化,保存时:

  • 只重写它改过的部件(正文,以及被改动的页眉/页脚/批注/注释);
  • 其余部件(styles.xml、主题、字体表、设置、媒体、关系、自定义 XML、VBA、嵌入字体、OLE 对象)逐字节从原 ZIP 带过
  • 关系 ID、书签名、域代码、样式 ID、编号定义不被重命名或重排
  • 输出是 canonical OOXML:修订是 Word 审阅窗格认得的真 w:ins/w:del,主题色保持主题引用。

「无损」≠「字节级相同」

ZIP 容器会被重建、XML 会被重新序列化,所以空白与部件顺序可能不同。目标是语义保留:你没编辑的内容不会丢失或被重新解释。若某文档破坏了这一行为,按官方说法那是 bug,应附 .docx 提 issue。


进入 指南 · 进阶:基于 Yjs 的实时协同、受控批注同步、headless 服务端处理、DocumentAgent 与模板填充、内容控件。