富文本编辑需求往往复杂性较高,业务场景中常常需要结构化信息的嵌入,如用户提及、文件引用、图片模块等。这篇文章将从实战出发,深入介绍如何基于 ReactQuill 自定义 Blot,打造一个可插拔、可交互、可扩展的富文本组件体系。
功能1:从零实现 @提及模块
一、效果说明
在协作文档、评论区、社交输入框中,用户提及是一项非常常见的功能。当我们输入 @ 并选择一个用户时,富文本编辑器应当能以高亮样式显示用户名,并在后台存储结构化信息(如用户 ID),便于后续业务逻辑处理。
我们需要实现:
- 输入
@+ 关键词 ➜ 弹窗联想用户 - 插入后显示:蓝底高亮 + 不可编辑 + 可提取结构化数据
- 支持 Delta 格式存储 / 还原
插入结果示意:
<span class="mention-blot" data-id="u1" contenteditable="false">@lin</span>
二、功能目标拆解
| 需求点 | 技术实现 |
|---|---|
| 自定义 @ lin 的展示 | 自定义 Inline Blot |
| 插入时传入结构化数据 | insertEmbed('mention', { ... }) |
| 展示样式固定,不可编辑 | contenteditable="false" |
| 提交时提取所有提及用户 | getContents() + 解析 Delta |
| JSON 数据还原为内容 | setContents() |
三、具体实现
1. 定义 Mention Blot 类
说明:
blotName定义 Quill 中使用的 blot 类型名称,后续insertEmbed('mention', ...)就是依赖这个名称。create方法定义了如何将数据转为 DOM 节点,记得加上contenteditable=false,防止用户手动修改。value用于反解析 DOM 节点为结构化数据,在getContents()中就会保留。
import Quill from 'quill'
const Inline = Quill.import('blots/inline')
class MentionBlot extends Inline {
static blotName = 'mention'
static className = 'mention-blot'
static tagName = 'span'
static create(data: { id: string; name: string }) {
const node = super.create()
node.setAttribute('data-id', data.id)
node.setAttribute('contenteditable', 'false')
node.innerText = `@${data.name}`
return node
}
static value(node: HTMLElement) {
return {
id: node.getAttribute('data-id') || '',
name: node.innerText.replace(/^@/, '')
}
}
}
Quill.register(MentionBlot)
2. React 中插入 mention
说明:
- 通过
quillRef.current.getEditor()获取 Quill 实例,插入指定的 embed 内容。 - 插入后需要手动设置新光标位置,否则继续输入可能在组件前面。
import React, { useRef } from 'react'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
export const QuillEditor = () => {
const quillRef = useRef<any>()
const insertMention = () => {
const editor = quillRef.current?.getEditor()
const range = editor?.getSelection()
if (range) {
editor.insertEmbed(range.index, 'mention', {
id: 'u1',
name: 'lin'
})
editor.setSelection(range.index + 1)
}
}
return (
<>
<button onClick={insertMention}>插入 @lin</button>
<ReactQuill ref={quillRef} theme="snow" />
</>
)
}
3. 提取结构化数据
const delta = editor.getContents()
const mentions = delta.ops
.filter(op => op.insert && op.insert.mention)
.map(op => op.insert.mention)
// 输出:
// [{ id: 'u1', name: 'lin' }]
4. 从 JSON 恢复回富文本
const delta = {
ops: [
{ insert: '你好,' },
{ insert: { mention: { id: 'u1', name: 'lin' } } },
{ insert: ',欢迎!' }
]
}
editor.setContents(delta)
功能2:富文本框内放图片信息(图片 icon + 名称),点击弹出 Modal 预览
一、实现效果
有些业务场景中,我们不仅要展示图片,还要显示图片名称、图标,并且支持点击查看大图。例如在任务评论区中:
“请查阅这张设计图 [🖼 图1:主流程页面.png]”。
目标:
- 在富文本中以“图标 + 名称”的形式插入图片引用
- 点击引用项时弹出 Modal 预览大图
- 插入时、保存时都保留结构化数据(id, name, url)
二、实现
1. 自定义 ImageInfoBlot 类
说明:
- 和 Mention Blot 类似,结构化数据通过 DOM 属性传入,后续可提取。
- 设置
contenteditable=false避免破坏图片结构。 - 点击事件可以用监听容器的方式处理(委托),不直接在
create中注册。
import Quill from 'quill'
const Inline = Quill.import('blots/inline')
class ImageInfoBlot extends Inline {
static blotName = 'imageInfo'
static tagName = 'span'
static className = 'image-info-blot'
static create(value: { id: string; name: string; url: string }) {
const node = super.create()
node.setAttribute('data-id', value.id)
node.setAttribute('data-url', value.url)
node.setAttribute('data-name', value.name)
node.setAttribute('contenteditable', 'false')
node.innerHTML = `
<span style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;">
<img src="icon-url" width="14" />
<span>${value.name}</span>
</span>
`
// 点击事件由组件统一监听,这里不写 onclick
return node
}
static value(node: HTMLElement) {
return {
id: node.getAttribute('data-id') || '',
url: node.getAttribute('data-url') || '',
name: node.getAttribute('data-name') || '',
}
}
}
Quill.register(ImageInfoBlot)
2. React 富文本编辑器组件,监听点击显示 Modal
import React, { useRef, useEffect, useState } from 'react'
import ReactQuill from 'react-quill'
import { Modal } from 'antd'
export const QuillEditor = () => {
const quillRef = useRef<any>()
const [modalInfo, setModalInfo] = useState<null | { url: string; name: string }>(null)
useEffect(() => {
const editor = quillRef.current?.getEditor()
if (!editor) return
const container = editor.root
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement
const imageEl = target.closest('.image-info-blot') as HTMLElement
if (imageEl) {
const url = imageEl.getAttribute('data-url') || ''
const name = imageEl.getAttribute('data-name') || ''
if (url) {
setModalInfo({ url, name })
}
}
}
container.addEventListener('click', handleClick)
return () => container.removeEventListener('click', handleClick)
}, [])
return (
<>
<ReactQuill ref={quillRef} theme="snow" />
<Modal
open={!!modalInfo}
onCancel={() => setModalInfo(null)}
footer={null}
width={600}
title={modalInfo?.name}
>
<img
src={modalInfo?.url}
alt={modalInfo?.name}
style={{ width: '100%' }}
/>
</Modal>
</>
)
}
小结
通过自定义 Blot,可以实现的功能很多,比如文中介绍的两种功能:
- ✅ @提及用户:插入结构化用户信息,支持提交与回显
- ✅ 图片信息模块:显示图片名称 + 图标,点击弹出 Modal 预览
当然,这只是冰山一角,借助 Quill 的 Blot 机制,还可以实现:
- ✅ 任务卡片 / 项目引用块
- ✅ #话题、$变量 等标记模块
- ✅ 可拖拽/排序的结构化块
- ✅ 富文本模板动态渲染字段
- ✅ 粘贴内容结构清洗、自动识别模块
欢迎大家评论区补充~