在前段时间的工做中,我遇到了一个在桌面端和移动端进行图文混排编辑的需求。虽然若是只须要编辑纯文本和图片,不必定要使用富文本编辑器来实现。可是为了之后方便扩展,好比文本会有样式要求,我仍是用 Draft.js 实现了一个功能较基础的富文本编辑器。react
我将代码开源在了这个项目 draft-editor 中,也能够在这里在线预览。本文中我将介绍一下一些关于 Draft.js 的基础知识,并由此扩展到如何在 Draft.js 编辑器中插入图片功能的实现。git
Draft.js 是 Facebook 推出的用于 React 的富文本编辑器框架,初始化一个最基本的 Draft.js 的代码以下:github
import React from ‘react’; import ReactDOM from ‘react-dom’; import {Editor, EditorState} from ‘draft-js’; class MyEditor extends React.Component { constructor(props) { super(props); this.state = {editorState: EditorState.createEmpty()}; this.onChange = (editorState) => this.setState({editorState}); } render() { return ( <Editor editorState={this.state.editorState} onChange={this.onChange} /> ); } }
能够在这里查看。这里给 Editor
传入一个 editorState
属性,并绑定一个 onChange
事件,当发生编辑操做时,返回一个新的 editorState
。这样咱们就获得了一个能够进行基本的文本操做的编辑器了。npm
在说明什么是 EditorState
及 Draft.js 对于数据的存储方式以前,须要简略介绍一下 Immutables.js。数组
Draft.js 是利用 Immutable.js来保存数据的,正如其名,这是一种不可变的数据结构。对于一个 Immutable 的对象,你没法修改它自己,若想修改其值,只会返回一个新的修改后的对象。将这一点应用在编辑器上,用户的每一次修改都会生成一个最新的状态快照,就很容易实现撤销功能了。数据结构
在 Draft.js 的使用过程当中,可能会遇到下面这些数据结构。app
Map
相似于 js 中的对象,用 .set()
和 .get()
方法进行写和读。框架
const Immutable = require(‘immutable’); const framework = Immutable.Map({ name: 'React', age: 6 }); const newFramework = client.set('name', 'Vue'); console.log(framework.get('name'));
OrderedMap
混合了 object
和 array
的特色。经过使用 orderedMap.get(‘key’)
和orderedMap.set(‘key’, newValue)
这两个方法,能够将它当成一个普通的 object
来使用。但和 Map
的不一样点在于其中的 key 是按照被加入时的顺序排序的。dom
Record
也相似于 Map
,但有一些不一样之处。编辑器
record
一旦被初始化,就不能再添加新的 key 了record
实例添加默认值还有一点,immutable 的对象,提供了 toJS()
方法,可将其转成普通的 js 对象,这一方法在想查看其内部内容时很是有用。
Immutable.js 参考文章: Immutable Data with Immutable.js | Jscrambler Blog
在建立基本的编辑器的时候,咱们用到了 EditorState
。 EditorState
是编辑器最顶层的状态对象,它是一个 Immutable Record 对象,保存了编辑器中所有的状态信息,包括文本状态、选中状态等。
调用 editorState.toJS()
可将 immutable record 转换成一个普通的 object,打印出来以下:
简单地来看下其中的部份内容:
currentContent
是一个 ContentState
对象,存放的是当前编辑器中的内容selection
中是当前选中的状态redoStack
和 undoStack
就是撤销/重作栈,它是一个数组,存放的是 ContentState
类型的编辑器状态decorator
会寻找特定的模式,并用特定的组件渲染出来既然编辑器中的内容是存储在一个 ContentState
对象中,那么这个 ContentState
又是什么?
ContentState
也是一个 Immutable Record 对象,其中保存了编辑器里的所有内容和渲染先后的两个选中状态。能够经过 EditorState.getCurrentContent()
来获取当前的 ContentState
,一样调用 .toJS()
后将它打印出来看下:
blockMap
和 entityMap
里放置的就是编辑器中的 block
和 entity
,它们是构建 Draft 编辑器的砖瓦。
一个 ContentBlock
表示一个编辑器内容中的一个独立的 block,即视觉上独立的一块。
如下图的编辑器做为一个例子,图中的四个红框标出的部分都是 block。在平时阅读文章时,内容是以段落为单位的,在编辑器中每一个段落就是一个 block,如第一个和最后一个红框中的文字内容。第二个红框中是一张图片,它也是一个 block,但显示方式不一样于普通的 block,为了自定义它的显示方式还须要额外作一些工做,后面会加以详细说明。
还有一点须要稍做说明,第三个红框中虽然是空白,但它也是一个 block,只不过其中的文本为空而已。
此时,输出一下 convertToRaw(currentContent)
,看看其中的内容。注意这里的输出结构与上面的 currentContent.toJS()
略有所区别,这里只有 blocks
和 entityMap
这两项。
能够看到 blocks
这个数组中依次存放了各个 block
的信息,每个 block
都是一个 contentBlock
对象。
每一个 contentBlock
都有以下的几个属性值:
key
: 标识出这是哪个 blocktype
: 这是何种类型的 blocktext
: 其中的文字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
。
entityMap: { 0: { type: "image", mutability: "IMUTABLE", data: {} } }
以上介绍了 Draft.js 是如何对编辑器中的数据进行存储的,接下来会从代码实现的角度来讲明插入图片是如何实现的。
插入图片有着这样的流程:首先为图片建立一个 entity
,而后建立一个带有这个 entity
的新 EditorState
,而后更新便可。如下是关键部分的代码:
import { AtomicBlockUtils } from 'draft.js'; // ... const editorState = this.state.editorState; const contentState = editorState.getCurrentContent(); // 使用 `contentState.createEntity` 建立一个 `entity`,指定其 `type` 为 `image` const contentStateWithEntity = contentState.createEntity( ‘image’, ‘IMMUTABLE’, { src } ); // 获取新建立的 `entity` 的 key const entityKey = contentStateWithEntity.getLastCreatedEntityKey(); // 用 `EditorState.set()` 来创建一个带有这个 `entity` 的新的 EditorState const newEditorState = EditorState.set( editorState, { currentContent: contentStateWithEntity }, ‘create-entity’ ); // 利用`AtomicBlockUtils.insertAtomicBlock` 来插入一个新的 `block` this.setState({ editorState: AtomicBlockUtils.insertAtomicBlock(newEditorState, entityKey, ‘ ‘) },
上面咱们已经见到了,一张图片是做为一个 atomic
类型的 block 插入的。Draft.js 提供了blockRendererFn
让咱们能够自定义 ContentBlock
的渲染方式,给它传入一个函数后,由该函数来判断这个 block 的 type
是什么,而后决定如何渲染。
如下的这段代码来自 Draft.js 的官方文档,展现了如何处理一个 type 为 atomic
的 ContentBlock
。
function myBlockRenderer(contentBlock) { const type = contentBlock.getType(); if (type === 'atomic') { return { component: MediaComponent, editable: false, props: { foo: 'bar', }, }; } } // Then... import {Editor} from 'draft-js'; class EditorWithMedia extends React.Component { ... render() { return <Editor ... blockRendererFn={myBlockRenderer} />; } }
能够看到这里传递了一个 props
:
component: MediaComponent, props: { foo: 'bar', },
结果等同于 <MediaComponent foo='bar' />
,能够利用这里的 props
传入所须要的其余数据。
这里咱们就能够定义一个本身的 MediaComponent
来决定展示方式。由于不论是图片仍是视频等其它的媒体类型,它们的 type
都是 atomic
。在 MediaComponent
里就须要经过 entity
的 type
来肯定其种类。
const entity = props.contentState.getEntity(props.block.getEntityAt(0)); const { src } = entity.getData(); // 取出图片的地址 const type = entity.getType(); // 判断 entity 的 type 的
当 entity
的 type
是咱们自定义的 image
时就能够返回 <Image />
组件了。
<Image src={src} /> // 自定义的图片组件 <Image />
既然已经插入了图片,那么如何删除它呢?固然咱们能够按键盘上的 Backspace 键来删除。也能够在图片的右上角加入一个 “X” 的图标,点击后删除该图片,实现方式以下。
deleteImage = (block) => { const editorState = this.state.editorState; const contentState = editorState.getCurrentContent(); const key = block.getKey(); const selection = editorState.getSelection(); const selectionOfAtomicBlock = selection.merge({ anchorKey: key, anchorOffset: 0, focusKey: key, focusOffset: block.getLength(), }); // 重写 entity 数据,将其从 block 中移除,防止这个 entity 还被其它的 block 引用 const contentStateWithoutEntity = Modifier.applyEntity(contentState, selectionOfAtomicBlock, null); const editorStateWithoutEntity = EditorState.push(editorState, contentStateWithoutEntity, ‘apply-entity’); // 移除 block const contentStateWithoutBlock = Modifier.removeRange(contentStateWithoutEntity, selectionOfAtomicBlock, ‘backward’); const newEditorState = EditorState.push(editorStateWithoutEntity, contentStateWithoutBlock, ‘remove-range’,); this.onChange(newEditorState); }
至此,对图片的相关操做就完成了。
在本文中,介绍了 Draft.js 的基本功能,它是如何进行数据的存储的,及 EditorState
、ContentState
、ContentBlock
、Entity
等对象间的关系。并以此为基础说明了如何在编辑器中对图片进行操做。
固然关于 Draft.js 还有不少内容没有在本文中说起,如修改行内文本的样式,利用 decorators
来插入与渲染连接等等。这些就须要读者探索下 Draft.js 的官方文档和其余人的分享并亲自尝试下了。
本文所基于的编辑器项目:draft-editorHow Draft.js Represents Rich Text Data
Building a Rich Text Editor with React and Draft.js, Part 2.4: Embedding Images
Draft.js 在知乎的实践