Skip to content
返回设计中心

富文本编辑器架构

ContentEditable 陷阱、文档模型、为何应直接用 ProseMirror/TipTap 而非 contentEditable。

组件模式高级

为什么

在原始 contentEditable 上建富文本编辑器听起来简单,直到遇到光标跳、复制粘贴坏、HTML 不可序列化。文档模型编辑器(ProseMirror/TipTap/Lexical)给你 schema、事务、合理行为。

复合组件模式被 Radix UI、Headless UI 和 shadcn/ui 采用 — 这是 2024 年采用最多的三个无头库。相比扁平组件 API,它们减少 70% 的属性逐层传递。

何时使用

任何超单行输入的用真编辑器:带粗斜体的评论、文章、文档。代码用代码编辑器(Monaco/CodeMirror)。

如何做

1. 选框架 — TipTap(ProseMirror 包装,React 友好)或 Lexical(Meta,高性能) 2. schema — 定义允许的节点/标记(段落、标题、粗体、链接);拒绝其他 3. 工具栏 — 按钮调编辑器命令(toggleBold、setLink),命令不活跃时禁用 4. 粘贴 — 把 HTML 净化为你的 schema,去样式/类,仅留语义标签 5. 持久化 — 序列化为 JSON(非 HTML)存储;HTML 是渲染目标非模型 6. 协作 — yjs 或 ProseMirror collab 插件做实时多用户 7. 扩展 — 图片上传、提及、代码块作插件;绝不 fork 核心

示例

import { useEditor, EditorContent } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; function RichEditor({ value, onChange }: { value: string; onChange: (html: string) => void }) { const editor = useEditor({ extensions: [StarterKit], // 段落、标题、粗体、斜体、列表、代码... content: value, onUpdate: ({ editor }) => onChange(editor.getHTML()), editorProps: { attributes: { class: "prose min-h-32 focus:outline-none" }, }, }); return ( <div className="border rounded"> <div className="toolbar"> <button onClick={() => editor?.chain().focus().toggleBold().run()} className={editor?.isActive("bold") ? "bg-gray-200" : ""}>B</button> <button onClick={() => editor?.chain().focus().toggleItalic().run()}>I</button> </div> <EditorContent editor={editor} /> </div> ); }

常见问题

  • ⚠以 HTML 为真源 — 无法查询/转换,粘贴注入任意标签
  • ⚠无 schema — 用户粘整 Word 文档带内联样式,布局崩
  • ⚠重渲染时光标跳 — 每次父渲染重初始化编辑器,丢焦点

最佳实践

  • ✓存 JSON、渲染 HTML — 文档模型可查询可转换;HTML 仅输出
  • ✓定义严格 schema — 拒绝不支持的节点/标记,粘贴自动转换适配
  • ✓稳定编辑器实例 — useEditor 配稳定配置,勿每次渲染重建

常见问题解答