Tiptap 深度教程 (一):理解 Tiptap 的设计哲学与核心架构

1,227 阅读11分钟

🎯 本文目标:带你深入理解 Tiptap 的核心设计思想,为后续的实战开发打下坚实的理论基础。

欢迎来到 Tiptap 深度教程系列!无论你是初次接触富文本编辑器,还是希望从其他库(如 Slate、Draft.js、Quill)迁移过来,本系列都将带你从核心概念到高级实战,全面掌握这款现代化、功能强大的编辑器工具包。

为什么要学习设计哲学?

在深入研究如何使用 Tiptap 的 API 之前,我们首先需要理解其背后的"为什么"——它的核心设计哲学。这不是纸上谈兵,而是实用主义:

  • 避免踩坑:理解设计理念能帮你避免用错误的方式使用 Tiptap
  • 提升效率:掌握核心概念后,你会发现很多功能的实现变得水到渠成
  • 架构决策:当你需要做技术选型或架构设计时,这些知识将成为重要的决策依据

Tiptap 的基石:ProseMirror

首先,一个重要的事实是:Tiptap 并非凭空创造,它屹立于一个巨人的肩膀之上——ProseMirror

什么是 ProseMirror?

ProseMirror 并非一个即插即用的编辑器,而是一个用于构建富文本编辑器的强大"工具集"。它由 Marijn Haverbeke(《Eloquent JavaScript》的作者)创建,以其严谨的文档结构和强大的可扩展性而闻名。

💡 类比理解:如果把富文本编辑器比作建房子,那么 ProseMirror 就是提供钢筋、水泥等建筑材料的供应商,而 Tiptap 则是使用这些材料为你建好房子的建筑公司。

Tiptap 继承了什么?

Tiptap 作为 ProseMirror 的一层"包装器"或"抽象层",直接继承了其诸多优点:

1. 结构化内容 (Schema-Driven)

传统编辑器的痛点

<!-- 传统编辑器可能产生这样混乱的HTML -->
<div>
  <span style="font-weight: bold;">
    <p>这样的嵌套是错误的</p>
  </span>
</div>

ProseMirror/Tiptap 的解决方案: 在 ProseMirror 中,文档内容不是一堆随意的 HTML,而是由严格的"模式 (Schema)"定义的结构化数据。

// Tiptap 的文档结构是可预测的
{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        {
          "type": "text",
          "text": "这是加粗文本",
          "marks": [{ "type": "bold" }]
        }
      ]
    }
  ]
}

🔍 深入理解:这种结构化的方式意味着你可以:

  • 轻松地序列化和反序列化内容
  • 进行精确的内容校验和过滤
  • 实现复杂的内容转换和处理逻辑
  • 保证在不同平台间的内容一致性

2. 强大的事务模型 (Transaction Model)

什么是事务? 事务就像银行转账——要么完全成功,要么完全失败,不会出现中间状态。

// 每次编辑都是一个完整的事务
editor.chain()
  .focus()                    // 1. 聚焦编辑器
  .toggleBold()              // 2. 切换粗体
  .setTextAlign('center')    // 3. 设置居中对齐
  .run()                     // 4. 执行整个事务

// 实际上,这个链式调用会创建一个事务:
// {
//   steps: [focusStep, toggleBoldStep, setTextAlignStep],
//   docs: [oldDoc, newDoc],
//   mapping: DocumentMapping
// }

事务模型的核心优势

  • 原子性:要么全部成功,要么全部失败
  • 可追溯性:每个更改都有完整的历史记录
  • 协作友好:多人编辑时冲突解决更加可靠
  • 性能优化:批量操作避免了多次重渲染

3. 虚拟 DOM 与增量更新

// ProseMirror 内部的更新机制
const oldState = editor.state
const newState = oldState.apply(transaction)

// 只更新真正发生变化的 DOM 节点
const diff = oldState.doc.diff(newState.doc)
updateDOM(diff) // 仅更新必要的部分

这确保了即使在处理大型文档时,编辑器依然保持流畅的性能。

4. 高性能与稳定性

Tiptap 无需重新发明轮子,而是直接利用了 ProseMirror 经过社区多年验证的、坚如磐石的核心:

  • 性能优化:ProseMirror 在大文档处理上有出色的性能表现
  • 稳定性保证:经过众多大型项目的实战检验(如 Atlassian、GitLab、Notion等)
  • 持续演进:跟随 ProseMirror 的更新获得新特性

深入理解:技术架构层级

┌─────────────────────────────────┐
│        你的应用层 (Your App)        │  ← 具体业务逻辑
├─────────────────────────────────┤
│       Tiptap 抽象层 (Tiptap)       │  ← 开发者友好的 API
├─────────────────────────────────┤
│      ProseMirror 核心层           │  ← 富文本编辑的底层引擎
├─────────────────────────────────┤
│         浏览器 DOM API            │  ← 原生浏览器能力
└─────────────────────────────────┘

每一层都有其独特的价值:

  • ProseMirror:提供强大的文档模型和编辑原语
  • Tiptap:提供开发者友好的 API 和丰富的扩展生态
  • 你的应用:专注于业务逻辑,无需关心底层实现细节

核心概念一:"无头 (Headless)"理念

"无头"是 Tiptap 最具标志性的特征之一,也是它与传统富文本编辑器的根本区别。

传统编辑器 vs 无头编辑器

传统编辑器的做法

// 传统编辑器(如 TinyMCE)
tinymce.init({
  selector: '#myeditor',
  plugins: 'advlist autolink lists link image',
  toolbar: 'undo redo | formatselect | bold italic | alignleft aligncenter alignright',
  // 工具栏样式、图标、布局都是固定的
});

Tiptap 的做法

// Tiptap 只提供编辑器核心
const editor = new Editor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})

// UI 完全由你决定
function MyToolbar() {
  return (
    <div className="my-custom-toolbar">
      <button onClick={() => editor.chain().focus().toggleBold().run()}>
        <MyBoldIcon /> {/* 你的图标 */}
      </button>
      <button onClick={() => editor.chain().focus().toggleItalic().run()}>
        <MyItalicIcon />
      </button>
    </div>
  )
}

"无头"理念的深层含义

1. 关注点分离 (Separation of Concerns)

┌─────────────────┐    ┌─────────────────┐
│   UI 表现层       │    │    编辑逻辑层     │
│                │    │                │
│ • 工具栏样式      │ ←→ │ • 文档状态       │
│ • 按钮布局       │    │ • 命令执行       │
│ • 主题配色       │    │ • 扩展管理       │
│ • 交互动画       │    │ • 事务处理       │
└─────────────────┘    └─────────────────┘
    (你负责)              (Tiptap负责)

2. 设计系统集成

// 与 Material UI 集成
import { Button, Toolbar } from '@mui/material'
import { FormatBold, FormatItalic } from '@mui/icons-material'

function MaterialUIToolbar({ editor }) {
  return (
    <Toolbar>
      <Button
        variant={editor.isActive('bold') ? 'contained' : 'outlined'}
        onClick={() => editor.chain().focus().toggleBold().run()}
      >
        <FormatBold />
      </Button>
    </Toolbar>
  )
}

// 与 Ant Design 集成
import { Button, Space } from 'antd'
import { BoldOutlined, ItalicOutlined } from '@ant-design/icons'

function AntdToolbar({ editor }) {
  return (
    <Space>
      <Button
        type={editor.isActive('bold') ? 'primary' : 'default'}
        icon={<BoldOutlined />}
        onClick={() => editor.chain().focus().toggleBold().run()}
      />
    </Space>
  )
}

"无头"带来的技术优势

1. 框架无关性 (Framework-Agnostic)

// React 项目
import { useEditor } from '@tiptap/react'

// Vue 项目  
import { useEditor } from '@tiptap/vue-3'

// Svelte 项目
import { Editor } from '@tiptap/core'

// 原生 JavaScript
import { Editor } from '@tiptap/core'

每个框架包装器都很轻量,只是提供了符合该框架约定的响应式绑定。

2. 渐进式增强

// 最简单的开始
const editor = new Editor({
  element: document.querySelector('#editor'),
  extensions: [Document, Paragraph, Text],
  content: 'Hello World!',
})

// 逐步添加功能
const editor = new Editor({
  // ...existing code...
  extensions: [
    Document,
    Paragraph, 
    Text,
    Bold,          // 添加粗体
    Italic,        // 添加斜体
    History,       // 添加撤销/重做
    // ...根据需要继续添加
  ],
})

3. 性能优化的极致控制

// 你可以精确控制何时更新 UI
const editor = new Editor({
  extensions: [StarterKit],
  onUpdate: ({ editor }) => {
    // 只在特定条件下更新状态
    if (shouldUpdateToolbar(editor)) {
      setToolbarState(getToolbarState(editor))
    }
  },
})

// 甚至可以自定义渲染策略
const editor = useEditor({
  extensions: [StarterKit],
  editorProps: {
    attributes: {
      class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none',
    },
  },
})

核心概念二:"万物皆扩展 (Extension-Based)"

如果说"无头"给了 Tiptap 自由,那么"基于扩展的架构"则给了它灵魂和无限的可能性。

在 Tiptap 中,所有功能——无一例外——都是通过扩展来实现的。从最基础的粗体斜体,到复杂的@提及、代码块、甚至是实时协作,每一个功能都是一个独立的、可插拔的"扩展"。

扩展的三大类型

1. 节点 (Nodes):文档的骨架

节点定义了文档的结构,通常是块级元素:

// 段落节点
const paragraph = {
  type: "paragraph",
  content: [
    { type: "text", text: "这是一个段落" }
  ]
}

// 标题节点
const heading = {
  type: "heading", 
  attrs: { level: 1 },
  content: [
    { type: "text", text: "这是一级标题" }
  ]
}

// 代码块节点
const codeBlock = {
  type: "codeBlock",
  attrs: { language: "javascript" },
  content: [
    { type: "text", text: "console.log('Hello World')" }
  ]
}

常见的节点扩展

  • Paragraph - 段落
  • Heading - 标题 (h1-h6)
  • CodeBlock - 代码块
  • Blockquote - 引用块
  • BulletList / OrderedList - 列表
  • Table - 表格
  • Image - 图片

2. 标记 (Marks):文本的装饰

标记为内联文本添加样式或元数据,但不改变文档结构:

// 带有多个标记的文本
{
  type: "text",
  text: "这是加粗且带链接的文本",
  marks: [
    { type: "bold" },
    { 
      type: "link", 
      attrs: { href: "https://example.com" }
    }
  ]
}

常见的标记扩展

  • Bold / Italic / Strike - 文字样式
  • Link - 链接
  • Code - 行内代码
  • TextStyle - 文字颜色/背景色
  • Subscript / Superscript - 上标/下标

3. 功能扩展 (Extensions):编辑器的能力

功能扩展提供不直接对应特定节点或标记的能力:

// 历史记录扩展配置
History.configure({
  depth: 100,        // 保存 100 步历史
  newGroupDelay: 500 // 500ms 内的操作算作一组
})

// 占位符扩展配置  
Placeholder.configure({
  placeholder: ({ node }) => {
    if (node.type.name === 'heading') {
      return '请输入标题...'
    }
    return '开始写作...'
  }
})

常见的功能扩展

  • History - 撤销/重做
  • Placeholder - 占位符提示
  • CharacterCount - 字符计数
  • Collaboration - 实时协作
  • Dropcursor - 拖拽光标
  • Gapcursor - 间隙光标

扩展架构的深层优势

1. 极致模块化与 Tree Shaking

// 最小配置 - 只包含必要功能
const minimalEditor = new Editor({
  extensions: [
    Document,
    Paragraph,
    Text,
    Bold,
    Italic,
  ]
})

// 完整配置 - 包含丰富功能
const fullEditor = new Editor({
  extensions: [
    StarterKit,
    Image,
    Link.configure({
      openOnClick: false,
    }),
    Placeholder.configure({
      placeholder: '开始写作...',
    }),
    CharacterCount.configure({
      limit: 10000,
    }),
  ]
})

2. 可预测的行为与调试

// 扩展配置一目了然
const editor = new Editor({
  extensions: [
    StarterKit.configure({
      // 禁用不需要的功能
      strike: false,
      history: false,
    }),
    History.configure({
      depth: 50, // 自定义历史深度
    }),
    CustomMention.configure({
      suggestion: mentionSuggestion,
    }),
  ],
  // 清晰的扩展依赖关系
  onSelectionUpdate: ({ editor }) => {
    console.log('当前激活的标记:', editor.getAttributes())
  },
})

3. 扩展的生命周期与钩子

// 自定义扩展示例
const CustomExtension = Extension.create({
  name: 'customExtension',
  
  // 扩展创建时
  onCreate() {
    console.log('扩展已创建')
  },
  
  // 编辑器创建时
  onBeforeCreate({ editor }) {
    console.log('编辑器即将创建')
  },
  
  // 编辑器创建后
  onCreate({ editor }) {
    console.log('编辑器已创建')
  },
  
  // 内容更新时
  onUpdate({ editor }) {
    console.log('内容已更新')
  },
  
  // 选区变化时
  onSelectionUpdate({ editor }) {
    console.log('选区已变化')
  },
  
  // 编辑器销毁时
  onDestroy() {
    console.log('编辑器已销毁')
  },
})

自定义扩展:无限可能

// 创建一个简单的高亮扩展
const Highlight = Mark.create({
  name: 'highlight',
  
  addOptions() {
    return {
      multicolor: false,
      HTMLAttributes: {},
    }
  },
  
  addAttributes() {
    if (!this.options.multicolor) {
      return {}
    }
    
    return {
      color: {
        default: null,
        parseHTML: element => element.getAttribute('data-color'),
        renderHTML: attributes => {
          if (!attributes.color) {
            return {}
          }
          return {
            'data-color': attributes.color,
            style: `background-color: ${attributes.color}`,
          }
        },
      },
    }
  },
  
  parseHTML() {
    return [
      {
        tag: 'mark',
      },
    ]
  },
  
  renderHTML({ HTMLAttributes }) {
    return ['mark', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
  },
  
  addCommands() {
    return {
      setHighlight: attributes => ({ commands }) => {
        return commands.setMark(this.name, attributes)
      },
      toggleHighlight: attributes => ({ commands }) => {
        return commands.toggleMark(this.name, attributes)
      },
      unsetHighlight: () => ({ commands }) => {
        return commands.unsetMark(this.name)
      },
    }
  },
})

与其他编辑器的对比分析

Tiptap vs Slate.js

特性TiptapSlate.js
底层架构基于 ProseMirror自研架构
学习曲线中等(得益于 ProseMirror 的成熟度)陡峭(需要理解 Slate 的独特概念)
生态系统丰富的官方扩展较少的现成组件
性能优秀(ProseMirror 优化)良好(但需要手动优化)
协作支持原生支持(Y.js)需要自行实现
自定义度高(扩展系统)极高(完全自定义)

Tiptap vs Quill

特性TiptapQuill
UI 哲学无头,完全自定义提供默认主题
扩展性强(基于扩展架构)中等(Delta 系统)
框架集成原生支持多框架需要包装器
文档模型结构化(Schema)基于 Delta
移动端优秀良好

融会贯通:设计哲学的实际应用

现在,让我们将三个核心概念串联起来,看看一个典型的交互流程:

// 1. 用户点击工具栏按钮(你的自定义 UI)
function ToolbarButton({ editor }) {
  return (
    <button 
      className={`btn ${editor.isActive('bold') ? 'active' : ''}`}
      onClick={() => {
        // 2. 调用 Tiptap 命令
        editor.chain().focus().toggleBold().run()
      }}
    >
      <BoldIcon />
    </button>
  )
}

// 3. Bold 扩展处理命令
const Bold = Mark.create({
  name: 'bold',
  addCommands() {
    return {
      toggleBold: () => ({ commands }) => {
        // 4. 创建 ProseMirror 事务
        return commands.toggleMark(this.name)
      },
    }
  },
})

// 5. 事务应用后,触发更新
const editor = new Editor({
  extensions: [Bold],
  onUpdate: ({ editor }) => {
    // 6. 你的 React/Vue 组件重新渲染
    console.log('文档已更新:', editor.getHTML())
  },
})

这个流程完美展示了:

  • 无头设计:UI 完全由你控制
  • 扩展架构:功能通过 Bold 扩展实现
  • ProseMirror 基础:底层事务处理

深入思考:为什么这种设计是革命性的?

1. 解决了传统编辑器的核心痛点

传统编辑器的问题

<!-- 混乱的 HTML 结构 -->
<div contenteditable>
  <span style="font-weight: bold;">
    <div>错误的嵌套</div>
  </span>
  <br><br><br> <!-- 大量的空行 -->
  <font color="red">过时的标签</font>
</div>

Tiptap 的解决方案

// 结构化的文档模型
{
  type: "doc",
  content: [
    {
      type: "paragraph",
      content: [
        {
          type: "text",
          text: "正确的结构",
          marks: [{ type: "bold" }]
        }
      ]
    }
  ]
}

2. 适应现代前端开发模式

// 组件化开发
function EditorWithToolbar() {
  const editor = useEditor({
    extensions: [StarterKit],
  })
  
  return (
    <div className="editor-container">
      <Toolbar editor={editor} />
      <EditorContent editor={editor} />
      <StatusBar editor={editor} />
    </div>
  )
}

// 状态管理集成
function EditorWithRedux() {
  const dispatch = useDispatch()
  const editor = useEditor({
    extensions: [StarterKit],
    onUpdate: ({ editor }) => {
      dispatch(updateDocument(editor.getJSON()))
    },
  })
  
  return <EditorContent editor={editor} />
}

3. 为未来做好准备

// AI 集成示例
const AIWritingAssistant = Extension.create({
  name: 'aiAssistant',
  
  addCommands() {
    return {
      generateText: (prompt) => ({ editor, view, state }) => {
        // 调用 AI API
        return aiService.generate(prompt).then(text => {
          // 插入生成的文本
          editor.commands.insertContent(text)
        })
      },
    }
  },
})

// 实时协作 + AI 的未来编辑器
const FutureEditor = new Editor({
  extensions: [
    StarterKit,
    Collaboration.configure({ document: ydoc }),
    AIWritingAssistant,
    VoiceInput,
    GestureRecognition,
  ],
})

总结与展望

通过本文的深入分析,我们理解了 Tiptap 的三大设计支柱及其深层含义:

🏗️ 基于 ProseMirror

  • 提供了坚实、可靠的底层基础
  • 继承了结构化内容、事务模型、虚拟DOM等核心优势
  • 确保了高性能和稳定性

🎨 无头理念

  • 赋予了开发者绝对的 UI 控制权和设计自由
  • 实现了真正的框架无关性
  • 支持渐进式增强和性能优化

🧩 万物皆扩展

  • 带来了极致的模块化和无限的可扩展性
  • 通过节点、标记、功能扩展的清晰分工
  • 支持自定义扩展满足特殊需求

🚀 设计哲学的价值

这种设计哲学不仅解决了当前的问题,更为未来的发展奠定了基础:

  1. 可维护性:清晰的架构让大型项目的维护变得简单
  2. 可扩展性:扩展系统让功能增加变得自然
  3. 可预测性:结构化的文档模型让行为变得可靠
  4. 面向未来:开放的架构能够适应未来的技术发展

🎯 关键洞察:Tiptap 不仅仅是一个编辑器,它是一个编辑器构建平台。它给了你足够的自由度来构建任何你想要的编辑体验,同时又提供了足够的结构来确保稳定性和性能。

在下一篇文章**《Tiptap 深度教程 (二):从零搭建你的第一个编辑器》**中,我们将把这些理论付诸实践,手把手教你:

  • 初始化一个基本的 Tiptap 编辑器
  • 理解和使用 StarterKit 扩展集
  • 构建自定义的工具栏和 UI
  • 处理常见的编辑器事件和状态管理
  • 亲身体验这些设计哲学在代码中的具体体现

牢记这些核心概念,在后续的学习和实践中你将会事半功倍。让我们一起探索 Tiptap 的无限可能!


💡 延伸阅读