探索 Tiptap V3 的最新功能

从 Draft.js 迁移到 Tiptap

Editor

Tiptap 是 Draft.js 的流行替代方案,且从 Draft.js 迁移到 Tiptap 非常直接。本指南将帮助你从 Draft.js 的不可变状态模型过渡到 Tiptap 的扩展系统。

内容迁移

HTML 内容兼容性

Draft.js 使用 ContentState,需要转换为 HTML 以供 Tiptap 使用:

// 将 Draft.js ContentState 转换为 HTML
import { convertFromRaw } from 'draft-js'
import { stateToHTML } from 'draft-js-export-html'

// 如果你已有 ContentState
const htmlContent = stateToHTML(contentState)

// 如果你有原始状态(JSON)
const contentState = convertFromRaw(rawContentState)
const htmlContent = stateToHTML(contentState)

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

如果你已有 Draft.js 输出的 HTML,也可以直接使用:

// 你现有的 Draft.js HTML 内容
const existingContent = '<p>Hello <strong>world</strong>!</p>'

// 在 Tiptap 中直接使用
const editor = new Editor({
  content: existingContent,
  extensions: [StarterKit],
})

尽管 HTML 完全可用,但我们建议将其转换为 Tiptap 的 JSON 格式,以获得更好的性能和可读性。对于现有内容的批量转换,可以使用HTML 工具以编程方式将 HTML 转换为 JSON。

编辑器设置

安装

首先,安装 Tiptap 及其依赖:

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

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

基本编辑器设置

将你的 Draft.js 编辑器替换为 Tiptap:

// Draft.js(迁移前)
import React, { useState } from 'react'
import { Editor, EditorState, RichUtils } from 'draft-js'

function MyDraftEditor() {
  const [editorState, setEditorState] = useState(EditorState.createEmpty())

  const handleKeyCommand = (command) => {
    const newState = RichUtils.handleKeyCommand(editorState, command)
    if (newState) {
      setEditorState(newState)
      return 'handled'
    }
    return 'not-handled'
  }

  return (
    <Editor
      editorState={editorState}
      onChange={setEditorState}
      handleKeyCommand={handleKeyCommand}
    />
  )
}

// Tiptap(迁移后)
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function MyTiptapEditor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  })

  return <EditorContent editor={editor} />
}

扩展

理解 Tiptap 的扩展系统

Tiptap 使用模块化扩展系统,类似于 Draft.js 的实体和装饰器系统。每个功能都是独立的扩展,具备清晰的 API。

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

请浏览我们的扩展示例查看所有可用扩展,或创建自定义扩展支持自定义功能和 HTML 元素。

常见 Draft.js 功能对应关系

Draft.js 功能Tiptap 扩展说明
粗体内联样式Bold包含在 StarterKit 中
斜体内联样式Italic包含在 StarterKit 中
下划线内联样式Underline包含在 StarterKit 中
代码内联样式Code包含在 StarterKit 中
链接实体Link包含在 StarterKit 中
图片实体Image单独提供
标题块Heading包含在 StarterKit 中
引用块Blockquote包含在 StarterKit 中
代码块CodeBlock包含在 StarterKit 中
无序列表BulletList, ListItem包含在 StarterKit 中
有序列表OrderedList, ListItem包含在 StarterKit 中

扩展配置

import { useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Image from '@tiptap/extension-image'
import { Color } from '@tiptap/extension-color'
import TextStyle from '@tiptap/extension-text-style'
import Highlight from '@tiptap/extension-highlight'

const editor = useEditor({
  extensions: [
    StarterKit,
    Image.configure({
      inline: true,
      allowBase64: true,
    }),
    TextStyle,
    Color.configure({
      types: [TextStyle.name],
    }),
    Highlight.configure({
      multicolor: true,
    }),
  ],
})

自定义扩展

对于 Draft.js 的自定义实体或装饰器,可创建 Tiptap 自定义扩展。详情请参阅我们的自定义扩展指南

UI 实现

工具栏实现

Draft.js 基于 RichUtils 的工具栏对应为 Tiptap 的 UI 组件:

// Draft.js 工具栏(迁移前)
import { RichUtils } from 'draft-js'

function DraftToolbar({ editorState, onChange }) {
  const toggleInlineStyle = (style) => {
    onChange(RichUtils.toggleInlineStyle(editorState, style))
  }

  const toggleBlockType = (blockType) => {
    onChange(RichUtils.toggleBlockType(editorState, blockType))
  }

  return (
    <div>
      <button onClick={() => toggleInlineStyle('BOLD')}>Bold</button>
      <button onClick={() => toggleInlineStyle('ITALIC')}>Italic</button>
      <button onClick={() => toggleBlockType('header-one')}>H1</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().toggleHeading({ level: 1 }).run()}
        className={editor.isActive('heading', { level: 1 }) ? 'active' : ''}
      >
        H1
      </button>
    </div>
  )
}

预构建 UI 组件

为了加速开发,可以使用 Tiptap 预构建的 UI 组件:

行内工具栏

创建类似于 Draft.js 行内样式的行内工具栏:

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={() => editor.chain().focus().toggleUnderline().run()}
          className={editor.isActive('underline') ? 'active' : ''}
        >
          Underline
        </button>
      </BubbleMenu>
    </>
  )
}

实体替换(节点视图)

Draft.js 实体可以被 Tiptap 的节点视图替换:

// Draft.js 实体(迁移前)
const LinkEntity = (props) => {
  const { url } = props.contentState.getEntity(props.entityKey).getData()
  return <a href={url}>{props.children}</a>
}

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

const LinkComponent = ({ node }) => {
  return <a href={node.attrs.href}>{node.textContent}</a>
}

const CustomLink = Node.create({
  name: 'customLink',

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

迁移清单

后续步骤