探索 Tiptap V3 的最新功能

从 Lexical 迁移到 Tiptap

Editor

用更好的替代方案如 Tiptap 替换你的 Lexical 编辑器。从 Lexical 迁移到 Tiptap 非常简单。本指南将帮助你从 Lexical 的基于节点的架构过渡到 Tiptap 的扩展系统。

内容迁移

HTML 内容兼容性

Lexical 使用自己的 JSON 结构,需要转换为 Tiptap 可用的格式。你可以将 Lexical 内容序列化为 HTML:

// 将 Lexical JSON 转换为 HTML
import { $generateHtmlFromNodes } from '@lexical/html'
import { $getRoot } from 'lexical'

// 假设你已有一个 Lexical 编辑器实例
const htmlContent = editor.update(() => {
  const root = $getRoot()
  return $generateHtmlFromNodes(editor, root)
})

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

如果你已经有 Lexical 输出的 HTML,可以直接使用:

// 你现有的 Lexical 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。请参照我们的安装指南中的框架特定安装说明。

基本编辑器设置

用 Tiptap 替换 Lexical 编辑器:

// Lexical(迁移前)
import { createEditor } from 'lexical'
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
import { ContentEditable } from '@lexical/react/LexicalContentEditable'

const initialConfig = {
  namespace: 'MyEditor',
  theme: {},
  onError: console.error,
}

function MyLexicalEditor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>请输入内容...</div>}
      />
    </LexicalComposer>
  )
}

// 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 采用模块化扩展系统,类似于 Lexical 的节点和插件架构。每个功能都是独立的扩展,具有清晰的 API。

StarterKit 包含所有基础扩展,你可根据需要添加或删除其他扩展。

在我们的扩展指南中探索所有可用扩展,或创建自定义扩展以支持自定义功能和 HTML 元素。

常见 Lexical 插件对应关系

Lexical 插件/节点Tiptap 扩展备注
@lexical/rich-textBoldItalic包含在 StarterKit 中
@lexical/linkLink包含在 StarterKit 中
@lexical/listBulletListOrderedListListItem包含在 StarterKit 中
@lexical/codeCodeCodeBlock包含在 StarterKit 中
@lexical/tableTable单独提供
HeadingNodeHeading包含在 StarterKit 中
QuoteNodeBlockquote包含在 StarterKit 中
ImageNodeImage单独提供
@lexical/historyHistory包含在 StarterKit 中
@lexical/textTextStyleColor单独提供

扩展配置

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'
import { Color } from '@tiptap/extension-color'
import TextStyle from '@tiptap/extension-text-style'

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

自定义扩展

对于 Lexical 自定义节点或插件,请创建自定义 Tiptap 扩展。详细指导请参见我们的自定义扩展指南

UI 实现

工具栏实现

Lexical 的工具栏插件在 Tiptap 中对应自定义 UI 组件:

// Lexical 工具栏(迁移前)
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { FORMAT_TEXT_COMMAND } from 'lexical'

function ToolbarPlugin() {
  const [editor] = useLexicalComposerContext()

  return (
    <div>
      <button
        onClick={() => {
          editor.dispatchCommand(FORMAT_TEXT_COMMAND, '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' : ''}
      >
        粗体
      </button>
      <button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        className={editor.isActive('italic') ? 'active' : ''}
      >
        斜体
      </button>
      <button
        onClick={() => editor.chain().focus().toggleUnderline().run()}
        className={editor.isActive('underline') ? 'active' : ''}
      >
        下划线
      </button>
    </div>
  )
}

预制 UI 组件

为加快开发速度,可使用 Tiptap 的预制 UI 组件:

浮动工具栏

使用 Tiptap 的 BubbleMenu 来复制 Lexical 的浮动工具栏:

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' : ''}
        >
          粗体
        </button>
        <button
          onClick={() => editor.chain().focus().toggleItalic().run()}
          className={editor.isActive('italic') ? 'active' : ''}
        >
          斜体
        </button>
        <button
          onClick={() => {
            const url = window.prompt('URL')
            if (url) {
              editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
            }
          }}
          className={editor.isActive('link') ? 'active' : ''}
        >
          链接
        </button>
      </BubbleMenu>
    </>
  )
}

节点视图(自定义节点)

Lexical 的自定义节点可用 Tiptap 的节点视图替代:

// Lexical 自定义节点(迁移前)
class ImageNode extends DecoratorNode {
  static getType() {
    return 'image'
  }

  createDOM() {
    const img = document.createElement('img')
    img.src = this.__src
    return img
  }
}

// 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)
  },
})

迁移检查清单

后续步骤