一份极简的 Quill 富文本编辑器快速上手指南,30 分钟完成基础接入。
一、Quill 是什么?
Quill 是一个现代化的富文本编辑器,专为 Web 应用设计。
核心特性:
- 所见即所得的编辑体验
- 丰富的文本格式(粗体、斜体、列表等)
- 可扩展的工具栏
- 轻量高效(核心库仅 43KB)
适用场景:
- 文章编辑器
- 评论系统
- 表单内容输入
- 后台管理系统
二、核心概念速览
2.1 数据存储格式
Quill 原生输出 HTML 格式:
typescript
const html = quill.root.innerHTML
// 输出: "<p>这是<strong>加粗</strong>的内容</p>"
为什么选择 HTML?
- ✅ 直接展示,无需转换
- ✅ 支持丰富样式
- ✅ 与编辑器无缝集成
存储建议 : 数据库使用 TEXT 类型字段
2.2 图片处理机制
Quill 默认行为: 将图片转为 Base64 直接嵌入 HTML
html
<img src="data:image/png;base64,iVBORw0KGgo...">
优点 : 无需后端,立即可用 缺点: 文件体积大 33%,不适合生产环境
生产建议: 自定义上传到服务器,只保存 URL
2.3 安全防护
使用 dangerouslySetInnerHTML 渲染 HTML 存在 XSS 风险。
解决方案: 使用 DOMPurify 过滤
typescript
import DOMPurify from 'dompurify'
<div dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(userInput)
}} />
三、快速上手(30 分钟)
步骤 1: 安装依赖(2 分钟)
bash
# 安装核心库
yarn add quill dompurify
# 安装类型定义
yarn add -D @types/dompurify
# 在入口文件引入样式
# import 'quill/dist/quill.snow.css'
步骤 2: 创建基础组件(10 分钟)
创建文件 src/components/RichEditor/index.tsx:
tsx
import React, { useRef, useEffect } from 'react'
import { Modal } from 'antd'
import Quill from 'quill'
interface RichEditorProps {
open: boolean
title?: string
value?: string
onOk: (content: string) => void
onCancel: () => void
}
const RichEditor: React.FC<RichEditorProps> = ({
open,
title = '富文本编辑器',
value = '',
onOk,
onCancel
}) => {
const editorRef = useRef<HTMLDivElement>(null)
const quillRef = useRef<Quill | null>(null)
useEffect(() => {
if (open && editorRef.current && !quillRef.current) {
// 延迟初始化,确保 DOM 已渲染
const timer = setTimeout(() => {
if (editorRef.current) {
// 创建 Quill 实例
const quill = new Quill(editorRef.current, {
theme: 'snow',
placeholder: '请输入内容...',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ color: [] }, { background: [] }],
['link', 'image'],
['clean']
]
}
})
quillRef.current = quill
// 设置初始内容
if (value) {
quill.root.innerHTML = value
}
}
}, 200)
return () => clearTimeout(timer)
}
// 弹窗关闭时销毁实例
if (!open && quillRef.current) {
quillRef.current = null
if (editorRef.current) {
editorRef.current.innerHTML = ''
}
}
}, [open, value])
const handleOk = () => {
if (quillRef.current) {
const html = quillRef.current.root.innerHTML
onOk(html)
}
}
return (
<Modal
title={title}
open={open}
onOk={handleOk}
onCancel={onCancel}
width={800}
destroyOnClose
>
<div
ref={editorRef}
style={{
minHeight: '400px',
border: '1px solid #d9d9d9',
borderRadius: '4px'
}}
/>
</Modal>
)
}
export default RichEditor
关键点说明:
- 使用
useRef保持 Quill 实例 - 延迟 200ms 初始化,确保 Modal 渲染完成
- 弹窗关闭时销毁实例,避免内存泄漏
步骤 3: 集成到表单(15 分钟)
在页面中使用组件:
tsx
import { useState } from 'react'
import { Form, Button } from 'antd'
import DOMPurify from 'dompurify'
import RichEditor from '@/components/RichEditor'
const DemoPage = () => {
const [form] = Form.useForm()
const [editorOpen, setEditorOpen] = useState(false)
const handleEditorOk = (html: string) => {
form.setFieldsValue({ content: html })
setEditorOpen(false)
}
const onFinish = (values: any) => {
console.log('提交的数据:', values)
// 这里调用接口保存数据
}
return (
<Form form={form} onFinish={onFinish}>
<Form.Item label="内容" name="content">
<div>
{/* 显示区域 */}
<div
style={{
border: '1px solid #d9d9d9',
borderRadius: '4px',
padding: '12px',
minHeight: '100px',
backgroundColor: '#fafafa',
marginBottom: '8px',
cursor: 'pointer'
}}
onClick={() => setEditorOpen(true)}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
form.getFieldValue('content') ||
'<p style="color:#999">点击编辑内容</p>'
)
}}
/>
{/* 编辑按钮 */}
<Button onClick={() => setEditorOpen(true)}>
编辑
</Button>
</div>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
{/* 富文本编辑器 */}
<RichEditor
open={editorOpen}
title="内容编辑"
value={form.getFieldValue('content')}
onOk={handleEditorOk}
onCancel={() => setEditorOpen(false)}
/>
</Form>
)
}
export default DemoPage
效果:
- 点击显示区域或"编辑"按钮,打开编辑器
- 编辑完成点击"确定",内容自动更新
- 点击"提交",获取 HTML 内容
安全要点:
- ✅ 显示时使用
DOMPurify.sanitize()过滤 - ✅ 添加
ql-editorclass 保持样式一致
步骤 4: 自定义图片上传(可选)
如果需要上传图片到服务器而非使用 Base64:
tsx
// 在 RichEditor 组件中添加上传逻辑
import { message } from 'antd'
const RichEditor: React.FC<RichEditorProps> = (props) => {
// ... 其他代码
// 上传函数(需根据实际接口调整)
const uploadImage = async (file: File): Promise<string> => {
const formData = new FormData()
formData.append('file', file)
// 调用上传接口
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
const result = await response.json()
return result.data.url // 返回图片 URL
}
// 选择本地图片
const selectLocalImage = () => {
const input = document.createElement('input')
input.setAttribute('type', 'file')
input.setAttribute('accept', 'image/*')
input.click()
input.onchange = async () => {
const file = input.files?.[0]
if (!file) return
const quill = quillRef.current
if (!quill) return
// 文件大小验证
if (file.size > 5 * 1024 * 1024) {
message.error('图片大小不能超过 5MB')
return
}
try {
message.loading({ content: '上传中...', key: 'upload', duration: 0 })
// 上传到服务器
const imageUrl = await uploadImage(file)
// 获取光标位置
const range = quill.getSelection(true)
// 插入图片
quill.insertEmbed(range.index, 'image', imageUrl)
// 移动光标到图片后面
quill.setSelection(range.index + 1, 0)
message.success({ content: '上传成功', key: 'upload' })
} catch (error) {
message.error({ content: '上传失败', key: 'upload' })
}
}
}
// 修改 Quill 初始化配置
const quill = new Quill(editorRef.current, {
theme: 'snow',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline'],
['image'] // 图片按钮
],
handlers: {
// 自定义图片处理
image: selectLocalImage
}
}
}
})
// ... 其他代码
}
注意事项:
- 上传接口需要根据实际项目调整
- 返回的 URL 应该是可公开访问的地址
- 建议添加文件类型和大小验证
四、XSS 安全防护
为什么需要防护?
用户可能输入恶意脚本:
html
<p>正常内容 <script>alert('XSS攻击!')</script></p>
如果直接使用 dangerouslySetInnerHTML 渲染,浏览器会执行脚本。
解决方案:DOMPurify
配置过滤规则:
tsx
import DOMPurify from 'dompurify'
const sanitizedHtml = DOMPurify.sanitize(userInput, {
// 允许的标签
ALLOWED_TAGS: [
'p', 'br', 'div', 'span',
'strong', 'em', 'u', 's',
'h1', 'h2', 'h3',
'ul', 'ol', 'li',
'blockquote', 'code', 'pre',
'a', 'img'
],
// 允许的属性
ALLOWED_ATTR: [
'href', 'src', 'alt', 'target',
'class', 'style'
]
})
过滤效果:
html
<!-- 输入 -->
<p>文本 <script>alert('XSS')</script> 继续</p>
<img src=x onerror="alert('XSS')">
<!-- 过滤后 -->
<p>文本 继续</p>
<img src="x">
- ✅
<script>标签被移除 - ✅
onerror等事件属性被移除 - ✅ 合法标签和属性保留
快速测试
在浏览器控制台执行:
javascript
// 找到显示区域
const div = document.querySelector('.ql-editor')
// 注入测试 HTML
div.innerHTML = '<p>测试 <script>alert("XSS")</script> 结束</p>'
// 观察是否弹出 alert
// - 弹出 → 存在漏洞
// - 不弹 → 已被拦截
五、常见问题
Q1: 如何自定义工具栏?
修改 toolbar 配置:
typescript
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic'],
[{ list: 'ordered' }, { list: 'bullet' }],
['link']
// 只保留需要的按钮
]
}
Q2: 如何限制内容长度?
typescript
const handleOk = () => {
const html = quill.root.innerHTML
const text = quill.getText()
if (text.length > 5000) {
message.error('内容不能超过 5000 字')
return
}
onOk(html)
}
Q3: 如何实现字数统计?
typescript
useEffect(() => {
if (!quillRef.current) return
const quill = quillRef.current
quill.on('text-change', () => {
const text = quill.getText()
const wordCount = text.trim().length
console.log('当前字数:', wordCount)
})
}, [])
附录:参考资源
官方文档:
- Quill 官方文档: quilljs.com/docs/quickstart
- Quill API 参考: quilljs.com/docs/api
- DOMPurify: github.com/cure53/DOMPurify
技术栈:
- React 18+
- Ant Design 5+
- TypeScript 4.5+
至此,你已掌握 Quill 快速接入的核心知识! 🎉
现在开始动手实践吧!