
Quill 是一款 API 驱动、功能强大的现代富文本编辑器。它具有易于拓展、各平台表现一致性等优点。Quill 官方 1.0 版本于 2016 年 9 月发布,目前在 Github 上有41.8k Star。
官网地址:Quill - Your powerful rich text editor
github仓库地址:GitHub - slab/quill: Quill is a modern WYSIWYG editor built for compatibility and extensibility
api文档地址:API - Quill Rich Text Editor
Module配置
Quill 是以可拓展性和定制化为理念进行设计的。它尽可能简洁地实现了的编辑器的核心功能,并暴露接口供开发者进行定制化开发。这些核心功能就是通过 Module 来组织管理的。主要的 Module 有这些:
Parchment文档模型
Parchment 是 Quill 的文档模型,你可以把它类比为 HTML 中的 DOM 树。正如 DOM 树是由 DOM 节点构成,Parchment 则是由 Blots 构成。Quill 默认提供了几种常见的 Blots 例如:Block Blot、Inline Blot、Embed Blot。为了便于理解,你可以将其类比为 HTML 中的内联元素和块状元素。为了满足不同的格式化需求,Quill 允许我们自定义 Blot。现在我们来实现一个使文字变成斜体的功能:
- let Inline = Quill.import('blots/inline');
-
- class ItalicBlot extends Inline { }
- ItalicBlot.blotName = 'italic';
- ItalicBlot.tagName = 'em';
-
- Quill.register(ItalicBlot);
-
- const quill = new Quill('#editor');
- quill.formatText(0, 4, 'italic', true);
我们在实现一个自定义的 Blot 的时候,并不需要完全从零开始,可以继承自 Quill 默认提供的基础 Blot。一个最简单的 Blot 至少有两个最基本的静态属性,blotName 和 tagName。其中 blotName 在格式化的时候作为名字传入,而 tagName 则是创建的 HTML 元素的标签名,在本文示例中使用 em 标签实现斜体效果。
Delta数据格式
Delta 是一种用来描述文本内容和内容编辑的简单、极富表现力的格式。它是基于 JOSN 格式,易于阅读和解析。Delta 可以用来描述任何富文本文档对象,并且它没有 HTML 的复杂性和语义不明确性。我们通过一个示例来看看 Delta 结构是什么样子的。假若文本编辑器的内容为:“Gandalf the Grey”,如你所见,第一个单词 “Gandalf” 是有粗体效果的,最后一个单词 “Grey” 则是斜体效果。那么代表文本编辑器内容的 Delta 则是如下格式:
- Delta = {
- ops: [
- { insert: 'Gandalf', attributes: { bold: true } },
- { insert: ' the ' },
- { insert: 'Grey', attributes: { italic: true } }
- ]
- }
'运行
那如果我们想把编辑器内容由 “Gandalf the Grey” 改为 “Gandalf theWhite”又该如何操作呢?答案很简单,保持前面 12 个字符不动,插入单词“White”并使文字变蓝,删除接下来的四个字符即可。在 Quill 编辑器中调用如下代码即可实现:
- const delta = new Delta().retain(12)
- .insert('White', { color: '#00F' })
- .delete(4);
- quill.updateContents(delta)
自定义上传图片需要添加自定义handlers来处理图片上传问题,添加了handlers后,编辑器不显示了,控制台还没有报错,不添加handlers还是会显示的:

使用useMemo将quill toolbar配置包裹起来就好了
- // quill toolbar
- const config = useMemo(() => {
- return {
- toolbar: {
- container: [
- [{ header: [1, 2, 3, 4, 5, false] }],
- [
- 'bold',
- 'italic',
- 'underline',
- 'strike',
- 'blockquote',
- 'image',
- ],
- [
- { list: 'ordered' },
- { list: 'bullet' },
- { color: [] },
- { background: [] },
- { align: [] },
- ],
- ],
- handlers: {
- image: uploadImages,
- },
- },
- }
- }, [])
有多种方式可以处理图片上传,每一种都是一个思路,1.比如监听剪切板粘贴图片的事件做处理,比如可以自定义img标签的handlers,2.或者直接监听编辑器的onChange事件,用正则匹配到图片标签后,或者监听delta变化后,将base64的图片转为file文件上传后,再将原来的img标签替换为线上链接的形式。至于哪种好,各有千秋吧
我这里选择使用第二种方式处理这些图片,原因有以下几种:
1.不用再单独处理是通过点击图片icon上传的图片还是粘贴过来的图片,统一处理delta变化处理,
2.兼容之前已经上传过的base64文章内容图片处理,如果之前已经有历史文章包含了base64的图片,也可以兼容处理了,将他们处理为链接的形式。
使用到的api有:
因为我使用的是react-quill,所以可以参考api:GitHub - zenoamaro/react-quill: A Quill component for React.
onChange:文档内容发生改变事件,可以拿到文档内容,delta数据,发生改变的原因,还有编辑器只读代理
getContents:可以获取到delta格式的文档数据内容
setContents:使用delta数据格式设置编辑器内容
还需要将base64的图片转为file然后上传到服务器中,转换函数:
-
- export const base64ToFile = (base64, fileName = `${Math.random()}`) => {
- let arr = base64.split(',')
- console.log(arr[0])
- let mime = arr[0].match(/:(.*?);/)[1]
- let bstr = atob(arr[1])
- let n = bstr.length
- let u8arr = new Uint8Array(n)
- while (n--) {
- u8arr[n] = bstr.charCodeAt(n)
- }
- console.log('mime-----', mime)
- return new File([u8arr], `${fileName}.${mime.split('/')[1]}`, {
- type: mime,
- })
- }
需要注意
1.content和delta和editor.getHTML()获取的都是实时的文本内容,不是quill绑定的value。
- const handleChangeQuill = (content, delta, source, editor) => {
- // content和delta和editor.getHTML()获取的都是实时的文本内容,不是quill绑定的value值
-
- .....
-
- }
2.setContents如果是粘贴图片,会导致文档指针定位回到最开始位置,需要单独处理。
3.setSelection可以设置文档指针位置,quill.setSelection(99999) 可以让指针一直处于文档末尾,但是需要根据情况处理,比如如果是用户从文档中间开始编辑的话,就设置到最底部.
4.insertEmbed可以在文档中某个位置插入新的文档节点标签,例如:
quill.insertEmbed(10, 'image', 'https://quilljs.com/images/cloud.png');
但是这种方法的前提还是需要先知道用户的光标位置。
监听文档变化,使用setContents更新文档内容,但是更新完之后,如果是粘贴图片会让用户的指针位置回到最开始的地方(这个问题的原因是粘贴上传图片,会导致编辑器失去焦点),这是一个问题需要解决。
解决的办法是:当文档变化的时候,让文档获取焦点
quill.focus()
所有代码:
- const quillEdit = useRef(null)
-
- // 编辑器获取焦点,获取指针位置
- const [editWho, setEditWho] = useState('')
- const [userIndex, setUserIndex] = useState(0)
-
- // 将光标定位到用户输入的位置
- useEffect(() => {
- console.log('富文本内容改变了', editWho, userIndex)
- const quill = quillEdit.current.getEditor()
- if (editWho === 'api' && userIndex !== 0) {
- quill.setSelection(userIndex + 2)
- }
- }, [htmlVal])
-
-
- const handleChangeQuill = (content, delta, source, editor) => {
- // content和delta和editor.getHTML()获取的都是实时的文本内容,不是quill绑定的value值
- let quill = quillEdit.current.getEditor()
- // 如果是用户粘贴图片,需要先获取焦点,然后获取到粘贴图片的位置,等图片上传完之后,再将光标恢复
- quill.focus()
- console.log('delta_ops------', content, delta, source, editor)
- let delta_ops = delta.ops
- let quilContent = editor.getContents()
- setHtmlValue(content)
- // 获取指针位置
- const range = quill.getSelection()
- // console.log('user index------', quill.hasFocus())
- if (range) {
- if (range.length == 0 && range.index !== 0) {
- console.log('User cursor is at index', range.index)
- setUserIndex(range.index)
- } else {
- const text = quill.getText(range.index, range.length)
- console.log('User has highlighted: ', text)
- }
- } else {
- console.log('User cursor is not in editor')
- }
- setEditWho(source)
- if (delta_ops && delta_ops.length > 0) {
- quilContent.ops.map((item) => {
- if (item.insert) {
- let imgStr = item.insert.image
- if (imgStr && imgStr?.includes('data:image/')) {
- let file = base64ToFile(imgStr)
- console.log('files=', file)
- let formData = new FormData()
- formData.append('file', file)
- // 调用文件上传接口-将二进制图片文件上传至服务器
- uploadFile(formData).then((res) => {
- // 将图片链接替换为服务器返回的图片链接
- item.insert.image = res.data[0]
- // 此方法会让用户指针回到最头部
- quill.setContents(quilContent)
- })
- }
- }
- })
- }
- }
-
-
-
-
-
- <ReactQuill
- theme="snow"
- ref={quillEdit}
- value={htmlVal}
- modules={config}
- placeholder="请输入图文详情"
- onChange={handleChangeQuill}
- />
最终的效果: