探索 Tiptap V3 的最新功能

从 Slate 迁移到 Tiptap

Editor

Tiptap 是一个流行的 Slate 替代方案,从 Slate 迁移到 Tiptap 十分简单。本指南将帮助你从 Slate 的 JSON 结构过渡到 Tiptap 的扩展系统。

内容迁移

HTML 内容兼容性

Slate 使用自定义的 JSON 结构,需要转换为 Tiptap 可用格式。你需要先将 Slate 内容序列化为 HTML:

// 使用 Slate 的序列化将 Slate JSON 转换为 HTML
import { serialize } from 'slate-html-serializer'

const rules = [
  {
    serialize(obj, children) {
      if (obj.object === 'block') {
        switch (obj.type) {
          case 'paragraph':
            return <p>{children}</p>
          case 'heading-one':
            return <h1>{children}</h1>
          case 'heading-two':
            return <h2>{children}</h2>
          // 需要的话添加更多块类型
        }
      }
      if (obj.object === 'mark') {
        switch (obj.type) {
          case 'bold':
            return <strong>{children}</strong>
          case 'italic':
            return <em>{children}</em>
          // 需要的话添加更多标记类型
        }
      }
    },
  },
]

const htmlContent = serialize(slateValue, { rules })

// 在 Tiptap 中使用 HTML 内容
const editor = new Editor({
  content: htmlContent,
  extensions: [StarterKit],
})

对于结构较简单的 Slate 内容,你也可以直接映射到 Tiptap 的 JSON 格式:

// 示例:Slate 到 Tiptap JSON 转换
function slateToTiptap(slateNodes) {
  return {
    type: 'doc',
    content: slateNodes.map((node) => {
      if (node.type === 'paragraph') {
        return {
          type: 'paragraph',
          content: node.children.map((child) => ({
            type: 'text',
            text: child.text,
            marks: child.bold ? [{ type: 'bold' }] : [],
          })),
        }
      }
      // 添加更多节点类型映射
    }),
  }
}

const tiptapContent = slateToTiptap(slateValue.document.nodes)
const editor = new Editor({
  content: tiptapContent,
  extensions: [StarterKit],
})

虽然 HTML 格式使用起来非常方便,我们建议转换为 Tiptap 的 JSON 格式以获得更好的性能和可读性。如果需要批量转换现有内容,可以使用 HTML 工具 以编程方式将 HTML 转为 JSON。

编辑器设置

安装

首先,安装 Tiptap 及其依赖:

npm install @tiptap/core @tiptap/starter-kit

Tiptap 支持所有现代前端 UI 框架,如 React 和 Vue。请参考我们框架特定的安装说明,见 安装指南

基础编辑器设置

替换你的 Slate 编辑器为 Tiptap:

// Slate(之前)
import { createEditor } from 'slate'
import { Slate, Editable, withReact } from 'slate-react'

const [editor] = useState(() => withReact(createEditor()))
const [value, setValue] = useState(initialValue)

return (
  <Slate editor={editor} value={value} onChange={setValue}>
    <Editable />
  </Slate>
)

// Tiptap(之后)
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

const editor = new Editor({
  element: document.querySelector('#editor'),
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})

扩展

理解 Tiptap 的扩展系统

Tiptap 采用模块化扩展系统,类似于 Slate 的插件系统。每个功能都是独立的扩展,具有清晰的 API 和配置选项。

StarterKit 是内含所有基础扩展的组合包,你也可根据需要添加或移除其他扩展。

浏览我们的 扩展指南 了解所有可用扩展,或 创建自定义扩展 支持自定义功能和 HTML 元素。

常见 Slate 插件对应关系

Slate 概念Tiptap 扩展说明
粗体标记Bold已包含于 StarterKit
斜体标记Italic已包含于 StarterKit
下划线标记Underline已包含于 StarterKit
链接标记Link已包含于 StarterKit
图片块Image独立提供
列表块BulletListOrderedListListItem已包含于 StarterKit
标题块Heading已包含于 StarterKit
引用块Blockquote已包含于 StarterKit
代码块CodeBlock已包含于 StarterKit
表格块Table独立提供

扩展配置

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Image from '@tiptap/extension-image'
import Table from '@tiptap/extension-table'
import TableRow from '@tiptap/extension-table-row'
import TableHeader from '@tiptap/extension-table-header'
import TableCell from '@tiptap/extension-table-cell'

const editor = new Editor({
  extensions: [
    StarterKit,
    Image.configure({
      inline: true,
      allowBase64: true,
    }),
    Table.configure({
      resizable: true,
    }),
    TableRow,
    TableHeader,
    TableCell,
  ],
})

自定义扩展

对于 Slate 的自定义插件,请创建 Tiptap 的自定义扩展。详见我们的 自定义扩展指南

UI 实现

工具栏实现

Slate 的自定义工具栏组件可以转换为 Tiptap 的 UI 组件:

// Slate 工具栏(之前)
const Toolbar = () => {
  const editor = useSlate()

  return (
    <div>
      <Button
        active={isMarkActive(editor, 'bold')}
        onMouseDown={(event) => {
          event.preventDefault()
          toggleMark(editor, 'bold')
        }}
      >
        Bold
      </Button>
    </div>
  )
}

// Tiptap 对应实现(React 示例)
function Toolbar({ editor }) {
  if (!editor) return null

  return (
    <div className="toolbar">
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        className={editor.isActive('bold') ? 'active' : ''}
      >
        Bold
      </button>
      <button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        className={editor.isActive('italic') ? 'active' : ''}
      >
        Italic
      </button>
      <button
        onClick={() => editor.chain().focus().toggleUnderline().run()}
        className={editor.isActive('underline') ? 'active' : ''}
      >
        Underline
      </button>
    </div>
  )
}

预构建 UI 组件

为了更快开发,使用 Tiptap 的预构建 UI 组件:

  • 浏览我们的 [UI 组件](/ui-components/getting-started/overview)获取现成组件
  • 查看我们的 默认文本编辑器示例 了解实用案例

悬浮工具栏

使用 Tiptap 的 BubbleMenu 模拟 Slate 的悬浮工具栏:

import { BubbleMenu } from '@tiptap/react'

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
  })

  return (
    <>
      <EditorContent editor={editor} />
      <BubbleMenu editor={editor}>
        <button
          onClick={() => editor.chain().focus().toggleBold().run()}
          className={editor.isActive('bold') ? 'active' : ''}
        >
          Bold
        </button>
        <button
          onClick={() => editor.chain().focus().toggleItalic().run()}
          className={editor.isActive('italic') ? 'active' : ''}
        >
          Italic
        </button>
        <button
          onClick={() => {
            const url = window.prompt('URL')
            if (url) {
              editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
            }
          }}
          className={editor.isActive('link') ? 'active' : ''}
        >
          Link
        </button>
      </BubbleMenu>
    </>
  )
}

节点视图(自定义元素)

Slate 的自定义元素可用 Tiptap 的节点视图(Node Views)替代。详见我们的 官方指南

// Slate 自定义元素(之前)
const ImageElement = ({ attributes, children, element }) => {
  return (
    <div {...attributes}>
      <img src={element.url} />
      {children}
    </div>
  )
}

// Tiptap 节点视图(之后)
import { Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'

const ImageComponent = ({ node }) => {
  return <img src={node.attrs.src} />
}

const CustomImage = Node.create({
  name: 'customImage',

  addNodeView() {
    return ReactNodeViewRenderer(ImageComponent)
  },
})

迁移清单

后续步骤