从 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-kitTiptap 支持所有现代前端 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-text | Bold,Italic | 包含在 StarterKit 中 |
@lexical/link | Link | 包含在 StarterKit 中 |
@lexical/list | BulletList,OrderedList,ListItem | 包含在 StarterKit 中 |
@lexical/code | Code,CodeBlock | 包含在 StarterKit 中 |
@lexical/table | Table | 单独提供 |
HeadingNode | Heading | 包含在 StarterKit 中 |
QuoteNode | Blockquote | 包含在 StarterKit 中 |
ImageNode | Image | 单独提供 |
@lexical/history | History | 包含在 StarterKit 中 |
@lexical/text | TextStyle,Color | 单独提供 |
扩展配置
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)
},
})