从插入图片功能的实现来介绍 Draft.js 富文本编辑器

在前段时间的工做中,我遇到了一个在桌面端和移动端进行图文混排编辑的需求。虽然若是只须要编辑纯文本和图片,不必定要使用富文本编辑器来实现。可是为了之后方便扩展,好比文本会有样式要求,我仍是用 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

Immutable.js 数据结构

在说明什么是 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 混合了 objectarray 的特色。经过使用 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

Draft 是如何存储数据的

什么是 EditorState

在建立基本的编辑器的时候,咱们用到了 EditorStateEditorState 是编辑器最顶层的状态对象,它是一个 Immutable Record 对象,保存了编辑器中所有的状态信息,包括文本状态、选中状态等。

调用 editorState.toJS() 可将 immutable record 转换成一个普通的 object,打印出来以下:

图片描述

简单地来看下其中的部份内容:

  • currentContent 是一个 ContentState 对象,存放的是当前编辑器中的内容
  • selection 中是当前选中的状态
  • redoStackundoStack 就是撤销/重作栈,它是一个数组,存放的是 ContentState 类型的编辑器状态
  • decorator 会寻找特定的模式,并用特定的组件渲染出来

什么是 ContentState

既然编辑器中的内容是存储在一个 ContentState 对象中,那么这个 ContentState 又是什么?

ContentState 也是一个 Immutable Record 对象,其中保存了编辑器里的所有内容和渲染先后的两个选中状态。能够经过 EditorState.getCurrentContent() 来获取当前的 ContentState,一样调用 .toJS() 后将它打印出来看下:

图片描述

blockMapentityMap 里放置的就是编辑器中的 blockentity,它们是构建 Draft 编辑器的砖瓦。

什么是 ContentBlock 和 Entity

一个 ContentBlock 表示一个编辑器内容中的一个独立的 block,即视觉上独立的一块。

如下图的编辑器做为一个例子,图中的四个红框标出的部分都是 block。在平时阅读文章时,内容是以段落为单位的,在编辑器中每一个段落就是一个 block,如第一个和最后一个红框中的文字内容。第二个红框中是一张图片,它也是一个 block,但显示方式不一样于普通的 block,为了自定义它的显示方式还须要额外作一些工做,后面会加以详细说明。

还有一点须要稍做说明,第三个红框中虽然是空白,但它也是一个 block,只不过其中的文本为空而已。

图片描述

此时,输出一下 convertToRaw(currentContent) ,看看其中的内容。注意这里的输出结构与上面的 currentContent.toJS() 略有所区别,这里只有 blocksentityMap 这两项。

图片描述

能够看到 blocks 这个数组中依次存放了各个 block 的信息,每个 block 都是一个 contentBlock 对象。

每一个 contentBlock 都有以下的几个属性值:

  • key: 标识出这是哪个 block
  • type: 这是何种类型的 block
  • text: 其中的文字
  • ……

Draft.js 中 blocktype 有 unstyled,paragraph,header-one,atomic …… 等值,在 Draft.js 的文档中 atomic 类型对应的是 <figure /> 元素,咱们也选取了它来实现插入图片的功能。

图中的这些 block 的除了第三个 key = “1u22q” 的 block 的 type 值是 atomic 外,其他的值都是 “unstyled”。再仔细看下这个 atomic 类型的 block:

图片描述

除了 keytexttype 等值以外,在 entityRanges 这一项中保存它保存了使用到的 entity 的信息:offset 和 length 肯定了 entity 在 block 中的范围,而 key 则能让咱们去取出对应的 entity

回到上面的打印出的 contentState的内容,除了 blocks 数组外还有一个 entityMap对象。它是以 entitykey 做为键值的对象,里面保存了图片、连接等种类的 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, ‘ ‘)
},

如何使用 blockRendererFn 来渲染图片

上面咱们已经见到了,一张图片是做为一个 atomic 类型的 block 插入的。Draft.js 提供了blockRendererFn 让咱们能够自定义 ContentBlock 的渲染方式,给它传入一个函数后,由该函数来判断这个 block 的 type 是什么,而后决定如何渲染。

如下的这段代码来自 Draft.js 的官方文档,展现了如何处理一个 type 为 atomicContentBlock

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 里就须要经过 entitytype 来肯定其种类。

const entity = props.contentState.getEntity(props.block.getEntityAt(0));
const { src } = entity.getData();    // 取出图片的地址
const type = entity.getType();  // 判断 entity 的 type 的

entitytype 是咱们自定义的 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 的基本功能,它是如何进行数据的存储的,及 EditorStateContentStateContentBlockEntity 等对象间的关系。并以此为基础说明了如何在编辑器中对图片进行操做。

固然关于 Draft.js 还有不少内容没有在本文中说起,如修改行内文本的样式,利用 decorators 来插入与渲染连接等等。这些就须要读者探索下 Draft.js 的官方文档和其余人的分享并亲自尝试下了。

参考文章及资源

本文所基于的编辑器项目:draft-editor

How Draft.js Represents Rich Text Data
Building a Rich Text Editor with React and Draft.js, Part 2.4: Embedding Images
Draft.js 在知乎的实践


本文原连接:从插入图片功能的实现来介绍如何用 Draft.js 编写富文本编辑器

相关文章
相关标签/搜索