從插入圖片功能的實現來介紹 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 編寫富文本編輯器

相關文章
相關標籤/搜索