在前段时间的工作中,我遇到了一个在桌面端和移动端进行图文混排编辑的需求。虽然如果只需要编辑纯文本和图片,不一定要使用富文本编辑器来实现。但是为了以后方便扩展,比如文本会有样式要求,我还是用 Draft.js 实现了一个功能较基础的富文本编辑器。
我将代码开源在了这个项目 draft-editor 中,也可以在这里在线预览。本文中我将介绍一下一些关于 Draft.js 的基础知识,并由此扩展到如何在 Draft.js 编辑器中插入图片功能的实现。
从一个基本的编辑器开始
Draft.js 是 Facebook 推出的用于 React 的富文本编辑器框架,初始化一个最基本的 Draft.js 的代码如下:
1 | import React from ‘react’; |
可以在这里查看。这里给
Editor
传入一个 editorState
属性,并绑定一个
onChange
事件,当发生编辑操作时,返回一个新的
editorState
。这样我们就得到了一个可以进行基本的文本操作的编辑器了。
Immutable.js 数据结构
在说明什么是 EditorState
及 Draft.js
对于数据的存储方式之前,需要简略介绍一下
Immutables.js。
Draft.js 是利用 Immutable.js来保存数据的,正如其名,这是一种不可变的数据结构。对于一个 Immutable 的对象,你无法修改它本身,若想修改其值,只会返回一个新的修改后的对象。将这一点应用在编辑器上,用户的每一次修改都会生成一个最新的状态快照,就很容易实现撤销功能了。
在 Draft.js 的使用过程中,可能会遇到下面这些数据结构。
Map
类似于 js 中的对象,用
.set()
和 .get()
方法进行写和读。
1 | const Immutable = require(‘immutable’); |
OrderedMap
混合了 object
和 array
的特点。通过使用
orderedMap.get(‘key’)
和
orderedMap.set(‘key’, newValue)
这两个方法,可以将它当成一个普通的 object
来使用。但和
Map
的不同点在于其中的 key 是按照被加入时的顺序排序的。
Record
也类似于
Map
,但有一些不同之处。 - 一个 record
一旦被初始化,就不能再添加新的 key 了 - 你可以给一个 record
实例添加默认值
还有一点,immutable 的对象,提供了
toJS()
方法,可将其转成普通的 js
对象,这一方法在想查看其内部内容时非常有用。
Immutable.js 参考文章:Immutable Data with Immutable.js | Jscrambler Blog
Draft 是如何存储数据的
什么是 EditorState
在创建基本的编辑器的时候,我们用到了 EditorState
。
EditorState
是编辑器最顶层的状态对象,它是一个 Immutable
Record
对象,保存了编辑器中全部的状态信息,包括文本状态、选中状态等。
调用 editorState.toJS()
可将 immutable record
转换成一个普通的 object,打印出来如下:
简单地来看下其中的部分内容: - currentContent
是一个
ContentState
对象,存放的是当前编辑器中的内容 -
selection
中是当前选中的状态 - redoStack
和
undoStack
就是撤销/重做栈,它是一个数组,存放的是
ContentState
类型的编辑器状态 - decorator
会寻找特定的模式,并用特定的组件渲染出来
什么是 ContentState
既然编辑器中的内容是存储在一个 ContentState
对象中,那么这个 ContentState
又是什么?
ContentState
也是一个 Immutable Record
对象,其中保存了编辑器里的全部内容和渲染前后的两个选中状态。可以通过
EditorState.getCurrentContent()
来获取当前的
ContentState
,同样调用 .toJS()
后将它打印出来看下:
blockMap
和 entityMap
里放置的就是编辑器中的 block
和
entity
,它们是构建 Draft 编辑器的砖瓦。
什么是 ContentBlock 和 Entity
一个 ContentBlock
表示一个编辑器内容中的一个独立的
block,即视觉上独立的一块。
以下图的编辑器作为一个例子,图中的四个红框标出的部分都是 block。在平时阅读文章时,内容是以段落为单位的,在编辑器中每个段落就是一个 block,如第一个和最后一个红框中的文字内容。第二个红框中是一张图片,它也是一个 block,但显示方式不同于普通的 block,为了自定义它的显示方式还需要额外做一些工作,后面会加以详细说明。
还有一点需要稍作说明,第三个红框中虽然是空白,但它也是一个 block,只不过其中的文本为空而已。
此时,输出一下 convertToRaw(currentContent)
,看看其中的内容。注意这里的输出结构与上面的
currentContent.toJS()
略有所区别,这里只有
blocks
和 entityMap
这两项。 可以看到
blocks
这个数组中依次存放了各个 block
的信息,每一个 block
都是一个 contentBlock
对象。
每个 contentBlock
都有如下的几个属性值: -
key
: 标识出这是哪一个 block - type
:
这是何种类型的 block - text
: 其中的文字 - ……
Draft.js 中 block
的 type
有
unstyled,paragraph,header-one,atomic …… 等值,在 Draft.js 的文档中
atomic
类型对应的是 <figure />
元素,我们也选取了它来实现插入图片的功能。
图中的这些 block 的除了第三个 key = “1u22q” 的 block 的 type 值是
atomic
外,其余的值都是
“unstyled”
。再仔细看下这个 atomic
类型的
block:
除了 key
,text
,type
等值之外,在 entityRanges
这一项中保存它保存了使用到的
entity
的信息:offset 和 length 确定了 entity
在 block 中的范围,而 key 则能让我们去取出对应的
entity
。
回到上面的打印出的 contentState
的内容,除了
blocks
数组外还有一个 entityMap
对象。它是以
entity
的 key
作为键值的对象,里面保存了图片、链接等种类的 entity
信息,从中就可获得 blocks
所需要的
entity
。
1 | entityMap: { |
以上介绍了 Draft.js 是如何对编辑器中的数据进行存储的,接下来会从代码实现的角度来说明插入图片是如何实现的。
插入图片的实现
如何插入图片
插入图片有着这样的流程:首先为图片创建一个
entity
,然后创建一个带有这个 entity
的新
EditorState
,然后更新即可。以下是关键部分的代码:
1 | import { AtomicBlockUtils } from 'draft.js'; |
如何使用 blockRendererFn 来渲染图片
上面我们已经见到了,一张图片是作为一个 atomic
类型的
block 插入的。Draft.js 提供了blockRendererFn
让我们可以自定义 ContentBlock
的渲染方式,给它传入一个函数后,由该函数来判断这个 block 的
type
是什么,然后决定如何渲染。
以下的这段代码来自 Draft.js
的官方文档,展示了如何处理一个 type 为 atomic
的
ContentBlock
。
1 | function myBlockRenderer(contentBlock) { |
可以看到这里传递了一个 props
:
1 | component: MediaComponent, |
结果等同于
<MediaComponent foo='bar' />
,可以利用这里的
props
传入所需要的其他数据。
这里我们就可以定义一个自己的 MediaComponent
来决定展现方式。因为不管是图片还是视频等其它的媒体类型,它们的
type
都是 atomic
。在
MediaComponent
里就需要通过 entity
的
type
来确定其种类。
1 | const entity = props.contentState.getEntity(props.block.getEntityAt(0)); |
当 entity
的 type
是我们自定义的
image
时就可以返回 <Image />
组件了。
1 | <Image src={src} /> // 自定义的图片组件 <Image /> |
如何删除一张图片
既然已经插入了图片,那么如何删除它呢?当然我们可以按键盘上的 Backspace 键来删除。也可以在图片的右上角加入一个 “X” 的图标,点击后删除该图片,实现方式如下。
1 | deleteImage = (block) => { |
至此,对图片的相关操作就完成了。
总结与其他
在本文中,介绍了 Draft.js 的基本功能,它是如何进行数据的存储的,及
EditorState
、ContentState
、ContentBlock
、Entity
等对象间的关系。并以此为基础说明了如何在编辑器中对图片进行操作。
当然关于 Draft.js
还有很多内容没有在本文中提及,如修改行内文本的样式,利用
decorators
来插入与渲染链接等等。这些就需要读者探索下
Draft.js 的官方文档和其他人的分享并亲自尝试下了。
参考文章及资源
How Draft.js Represents Rich Text Data Building a Rich Text Editor with React and Draft.js, Part 2.4: Embedding Images Draft.js 在知乎的实践