富文本编辑器Tiptap系列教程——5分钟搭建基于Tiptap的富文本编辑器
接上篇 富文本编辑器 Tiptap 系列教程——5 分钟认识 Tiptap,本节我们主要讲一下如何构建一个 Tiptap 富文本编辑器。
通过上篇文章,我们已经对 Tiptap 有了简单的认识,那么接下来我们动手实操一下。
快速搭建一个简单的 Tiptap 应用
-
使用 vite 创建 vue3 项目
-
安装依赖
Terminal window pnpm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit -
创建组件
<template><editor-content class="editor-container" :editor="editor" /></template><script setup>import { useEditor, EditorContent } from '@tiptap/vue-3'import StarterKit from '@tiptap/starter-kit'const editor = useEditor({content: '<p>在 Vue.js中运行 Tiptap 🎉</p>',extensions: [StarterKit],})</script><style scoped>.editor-container {width: 400px;height: 400px;}</style>此时启动项目就看到如下界面:

tiptap-demo.png
是不是很简单,我们只需要@tiptap/vue-3提供的EditorContent组件,然后定义要启用哪些扩展extensions,以及提供初始文档content,一个高度可定制的富文本编辑器就实现了 🎉。
初始化后,此时的编辑器只是光秃秃的一个输入框,与传统的富文本编辑器相比,还差一些功能,像:顶部菜单、自定义样式、输入输出等,这只需要我们一步步实现即可。
创建菜单
Tiptap 最大的特点:Headless,无头,高度可定制。
我们可以完全控制 Tiptap 的菜单、外观样式等,而且 Tiptap 提供了丰富的 API 来帮助我们打造自己的菜单。
Tiptap 中包含多种菜单样式:
- 固定菜单
- 气泡菜单
- 浮动菜单
- 斜杠
/菜单
固定菜单
固定菜单就是我们常见的各种富文本编辑器的顶部或底部菜单。
例如,我们只引入 StarterKit ,然后用它提供的节点和扩展来做一个简单的 demo:

富文本编辑的菜单一般有这几种状态:点击事件、不可点击状态、激活状态,我们看一下bold的代码:
<button @click="editor.chain().focus().toggleBold().run()" :disabled="!editor.can().chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }"> bold</button>Tiptap 中调用 API 可以像 Promise 一样链式调用,通过chain()就可以创建一条链,比如上面的示例,同时执行了focus()和toggleBold()方法,focus 可以让编辑器重新获取焦点,因为当单击内容之外的按钮时,编辑器会失焦,所以需要 focus 重新获取焦点,后面大多数命令都会添加 focus,toggleBold 将所选文本切换粗体/清除粗体标记,Tiptap 中大多数扩展都带有 set…()、unset…() 和 toggle…() 命令,很好理解。
注意:通过 chain 链式调用时需要添加 .run() 来实际执行所有命令。
那我们怎么管理当前菜单的激活状态呢 🤔️?
Tiptap 也提供了相应的 API isActive()检查是否已将某些标记应用于所选文本,返回 true 或 false,我们可以借助该功能来切换 CSS 类名。比如通过editor.isActive('bold')判断选择的内容是否含有 Bold 标记,如果所选文本跨越多个标记,或者只有部分选择有标记,isActive()将返回 false。
// 比较给定属性的示例editor.isActive('highlight', { color: '#ffa8a8' })// 支持正则表达式editor.isActive('textStyle', { color: /.*/ })// 比较标记的值editor.isActive({ textAlign: 'right' })气泡菜单
气泡菜单就是出现在所选文本附近的菜单,可以快速对所选文本进行设置。Tiptap 提供了Bubble Menu扩展来实现此功能。
-
安装
Terminal window npm install @tiptap/extension-bubble-menu -
选项
- element:菜单挂载点,HTML 节点
- updateDelay:更新延迟,默认 250ms
- tippyOptions:BubbleMenu 扩展基于 tippy.js,此参数将完全传递给它
- pluginKey:扩展唯一标识,项目暂时未用到
- shouldShow:自定义菜单是否显示
-
使用
<template><bubble-menuv-if="editor":editor="editor":tippy-options="{ duration: 100 }"><button@click="editor.chain().focus().toggleBold().run()":class="{ 'is-active': editor.isActive('bold') }">bold</button><button@click="editor.chain().focus().toggleItalic().run()":class="{ 'is-active': editor.isActive('italic') }">italic</button><button@click="editor.chain().focus().toggleStrike().run()":class="{ 'is-active': editor.isActive('strike') }">strike</button></bubble-menu></template><script setup>import { BubbleMenu, Editor } from '@tiptap/vue-3'defineProps({editor: {type: Editor,require: true,},})</script><style lang="scss" scoped></style>完了,当我们选择文本时就会出现气泡菜单

bubble-menu-demo.png -
自定义显示逻辑
上面说到可以通过 shouldShow 控制自定义菜单是否显示,那么我们可以修改代码如下:
<template><bubble-menuv-if="editor":editor="editor":tippy-options="{ duration: 100 }":should-show="shouldShow">。。。</bubble-menu></template><script setup>...const shouldShow = ({ editor, view, state, oldState, from, to }) => {// 仅在无序列表选中的时候才显示气泡菜单return editor.isActive("bulletList");};</script>这样只有当我们选中无序列表时才会显示气泡菜单。
浮动菜单
浮动菜单就是出现在空行的菜单,可以快速设置当前行。Tiptap 提供了Floating Menu扩展来实现此功能。
安装
npm install @tiptap/extension-floating-menu选项、展示逻辑都与气泡菜单相同,这里不再赘述。看下效果:

斜杠菜单
斜杠菜单还是一个实验性的扩展,使用 / 开始一个新行,并会弹出一个窗口以选择应添加哪个节点。基于suggestion

修改外观样式
Tiptap 是无头的,这意味着没有提供样式。同时也意味着,我们可以完全控制编辑器的外观。如设置编辑器的自定义样式。
-
通过容器设置 css 样式
编辑器最外层的容器中 class 为
.ProseMirror,可以通过这个类来自定义编辑器内部样式,完全由自己控制。/* 只作用在编辑器中 */.ProseMirror p {margin: 1em 0;} -
向扩展添加自定义类
大多数扩展都可以通过
HTMLAttributes选项将 class 类名添加到呈现的 HTML 元素上。所以可以使用它来添加自定义类(或任何其他属性)new Editor({extensions: [Document,Paragraph.configure({HTMLAttributes: {class: 'my-custom-paragraph',},}),Heading.configure({HTMLAttributes: {class: 'my-custom-heading',},}),Text,],})渲染到页面上如图所示:

custom-extension-class.png -
向编辑器添加自定义类
通过前面我们知道,编辑器实例的最外部容器类名为
.ProseMirror,我们也可以自定义它。在初始化 Editor 编辑器时,可以通过editorProps.attributes.class属性向编辑器添加 class,new Editor({editorProps: {attributes: {class: 'tiptap-prose',},},})渲染到页面上如图所示:

custom-editor-class.png -
自定义标记将节点
如可以自定义的粗体扩展,将默认呈现的
<strong>标签,改为呈现<b>标签:import Bold from '@tiptap/extension-bold'const CustomBold = Bold.extend({renderHTML({ HTMLAttributes }) {// Original:// return ['strong', HTMLAttributes, 0]return ['b', HTMLAttributes, 0]},})new Editor({extensions: [// …CustomBold,],})
custom-mark-class.png
Tiptap 输出和回显
也许已经有小伙伴想问很久了:Tiptap 的数据保存和回填是怎么做的呢? 这不就来了嘛 😂
Tiptap 编辑器的内容可以存储为 JSON 或 HTML 字符串,并且两者都可以用来传入编辑器进行内容回显。TIptap 提供了获取 JSON 和 HTML 字符串的 API:getJSON()和getHTML(),调用这两个方法,即可获得 JSON 格式或者 HTML 字符串的编辑器里的富文本内容。
保存输出结果
一般会有两种保存:实时保存和手动保存。
-
文档发生改变时保存
Tiptap 提供了
onUpdate来监听编辑器内容变化,通过getJSON()和getHTML()即可拿到最新数据。const editor = new Editor({onUpdate({ editor }) {const json = editor.getJSON()const html = editor.getHTML()console.log(json)console.log(html)},}) -
手动保存
通常是点击按钮时进行保存,此时通过初始化的
editor实例,调用上述方法即可拿到数据。
回显
回显时可以在编辑器初始化时设置content字段,或者调用setContent异步更新,JSON 格式或 HTML 格式都可以进行回填,并且官方提供了JSON 转 HTML 的 API。
而如果想将其他富文本编辑器内容转为 Tiptap 的内容,官方同样提供了一个 PHP 包来将 HTML 转换为兼容的 JSON 结构:ueberdosis/prosemirror-to-html。
// 编辑器初始化时设置new Editor({ content: `<p>示例文字</p>`,})
// or 后期异步更新设置editor.commands.setContent(json)协同操作
Tiptap 对 Y.js 提供一流的支持,添加实时协作、离线编辑或设备间同步等功能非常棒,当然此时就不能只是简单的 HTML 或 JSON 格式的存储了,以后再说,可以先看官方协同编辑文档。
Markdown
Tiptap 基于一些原因, 目前不支持 Markdown 作为输入或输出格式,如:HTML 或 JSON 格式是嵌套结构,而 Markdown 是扁平的。如果想实现要 Markdown,可以参考前面提到的Nextcloud Text。
我们也可以在官方 demo中看到更多案例,有很多都是可以直接拿过来用的。
最后
这篇文章通过搭建 Tiptap 应用,我们了解了 Tiptap 的一些 API、菜单样式以及 Tiptap 的输出格式。通过这篇文章,相信你已经可以搭建一个自己开发的富文本编辑器了,那还等什么?动手试试吧 😄。