Tiptap 提及功能实战:如何给编辑器加上 @用户 与 #标签
这篇带你把 Tiptap 提及功能真正跑起来:先敲两行代码看到 @ 弹出候选人,再弄清背后的 Suggestion 机制,最后扩展出 #标签、自定义弹层和配套样式,并提前绕开 3 个高频坑。代码都按"最小能跑"给,复制即用。
先跑一个最小 Demo 🏃
先装依赖:
npm install @tiptap/core @tiptap/extension-mention @tiptap/vue-3
一个 Vue 3 组件就是全部:
<template>
<editor-content :editor="editor" />
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Mention from '@tiptap/extension-mention'
export default {
components: { EditorContent },
data() {
return { editor: null }
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
Mention.configure({
HTMLAttributes: { class: 'mention' }, // 渲染到 span 上,样式全靠它
suggestion: {
char: '@', // 触发字符
items: ({ query }) => {
const users = [
{ id: '1', label: '张三' },
{ id: '2', label: '李四' },
{ id: '3', label: '王五' },
]
// 按输入过滤,最多给 5 条
return users
.filter(u => u.label.includes(query))
.slice(0, 5)
},
},
}),
],
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>
保存后在编辑器里敲 @,候选人弹层就会浮出来。想对照完整效果,可以看仓库里的 官方 Demo,把 items 换成接口请求就能直接上线。
触发背后发生了什么 🔍
机制其实不复杂。Mention 的 addProseMirrorPlugins 会把每份 suggestion 配置各变成一个独立的 ProseMirror 插件;光标附近有输入时,插件去匹配"触发字符 + 后续文本",命中后调用 items 拉候选、交给 render 画弹层;你选中某一项时执行 command,把这段输入整体替换成 mention 节点加一个空格。模块本体在 packages/extension-mention/,节点属性 MentionNodeAttrs(id、label、mentionSuggestionChar)和 renderText/renderHTML 都定义在 mention.ts 里。
扩展到 @ 和 # 多触发 🚦
单人触发用 suggestion 就够;要多触发,就换成 suggestions 数组,每项独立配置 char、pluginKey、items、command:
import { PluginKey } from '@tiptap/pm/state'
Mention.configure({
HTMLAttributes: { class: 'mention' },
suggestions: [
{
char: '@',
pluginKey: new PluginKey('user-mention'), // 每个触发器独立 key
items: ({ query }) => fetchUsers(query), // 换成你的用户接口
command: ({ editor, range, props }) => {
editor
.chain()
.focus()
.insertContentAt(range, [
{ type: 'mention', attrs: { ...props, mentionSuggestionChar: '@' } },
{ type: 'text', text: ' ' },
])
.run()
},
},
{
char: '#',
pluginKey: new PluginKey('tag-mention'),
items: ({ query }) => fetchTags(query),
// 不写 command,默认插入 mention 节点并补一个空格
},
],
})
几个要点。suggestions 一旦写了,suggestion 字段就被忽略,别两个都配。每个条目最好给独立的 pluginKey,两套触发才不会抢事件。节点上会记一个 mentionSuggestionChar 属性,renderText/renderHTML 靠它决定显示 @ 还是 #,所以两套触发渲染时不会串。参考实现见 MultiMention 示例。
把建议框换成自己的 UI 🎨
默认弹层够用,但想做成带头像、带分组的列表,就覆盖 render。它返回四个回调,构成弹层的完整生命周期:
render: () => {
let component
return {
onStart: props => { /* 用 VueRenderer 挂载弹层组件,按光标定位 */ },
onUpdate: props => { /* 候选变化或光标移动时刷新 */ },
onKeyDown: props => { /* 拦截 Esc / 方向键 / Enter */ },
onExit: () => { /* 移除并销毁组件 */ },
}
}
套路固定:onStart 里用 VueRenderer 把弹层组件挂到 document.body,并用光标位置换算出坐标;onUpdate 负责后续刷新;onExit 负责清理。完整可跑的写法在 官方 Demo 的 suggestion.js,里面还给了 debounce: 300、minQueryLength: 2 和 initialItems(刚敲 @ 还没输入时先垫几条)三个现成选项。
让提及标签好看一点 ✨
提及节点默认渲染成 <span data-type="mention">,把下面这段贴进样式文件即可:
.tiptap .mention {
background-color: #ece5ff;
color: #6a3de8;
border-radius: 6px;
padding: 2px 6px;
font-weight: 500;
box-decoration-break: clone; /* 换行时每段都带圆角背景 */
}
想区分 @ 用户和 # 标签的视觉,可以再用 [data-mention-suggestion-char] 选择器分别命中两种节点。
最容易踩的 3 个坑 ⚠️
坑一:删完 mention 还留一个 @。deleteTriggerWithBackspace 默认是 false,Backspace 删掉节点后会原地留下触发字符。想要连 @ 一起删掉:
Mention.configure({ deleteTriggerWithBackspace: true })
坑二:两个触发器互相抢事件。多触发时如果共用同一个 pluginKey,建议框的显示和退出会互相干扰。按上面写法给每项配不同的 new PluginKey(...) 即可。
坑三:CSS 写了没反应。选择器没对上就白写——它必须和 HTMLAttributes 里注册的 class 一致,或者直接拿默认就有的属性选择:
.tiptap span[data-type="mention"] { /* 这里才是生效的选择器 */ }
最后
Tiptap 用户提及的本质,是把 Suggestion 机制包成了一个可复用的节点。学会这套写法后,搜索插公式、引用代码片段这类需求也能照抄。下一步可以翻翻 packages/suggestion/ 的完整选项,或把 Mention 和 extension-link 组合,让被 @ 的名字直接带跳转,Tiptap 标签功能也就补齐了。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



