基于Draft.js自定義富文本編輯器

寫寫文章總結(jié)一下之前的工作內(nèi)容奏路,看來以后還是要及時(shí)寫總結(jié)费彼,現(xiàn)在寫好多細(xì)節(jié)都想不起來了??茫因。
公司小程序后臺(tái)管理頁面,由于業(yè)務(wù)需求需要自定義富文本編輯器用于文章格式的編輯牍疏。使用第三方的富文本編輯器改動(dòng)起來不太靈活蠢笋,經(jīng)過調(diào)研,決定使用facebook的開源庫(kù)Draft.js來自定義一個(gè)富文本編輯器麸澜。
Draft.js官網(wǎng)如下: https://draftjs.org,它是基于React開發(fā)的奏黑,并不是一個(gè)開箱即用的編輯器炊邦,如果你直接使用编矾,像這樣子:

import React from 'react';
import {Editor, EditorState} from 'draft-js';

class RichEditor 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} />
    );
  }
}

export default RichEditor;

這樣界面只會(huì)出現(xiàn)一個(gè)可編輯的空白行。Draft.js只提供基礎(chǔ)功能模塊馁害,開發(fā)者需要根據(jù)業(yè)務(wù)需求做進(jìn)一步的編碼窄俏。那么相比其他的富文本編輯器Draft.js有什么優(yōu)勢(shì)呢?要回答這個(gè)答案就要先了解它使用和存儲(chǔ)富文本的方式碘菜。

  1. EditorState與ContentState
    EditorState 是 Draft.js 最重要的一個(gè)對(duì)象凹蜈,它是用來存儲(chǔ)富文本編輯器所有內(nèi)容和狀態(tài)的。這個(gè)對(duì)象作為組件屬性輸入給 Editor 組件忍啸,一旦用戶進(jìn)行操作仰坦,比如敲一個(gè)回車,Editor 組件的 onChange 事件觸發(fā)计雌,onChange 函數(shù)返回一個(gè)全新的 EditorState 實(shí)例悄晃,Editor 接收這個(gè)新的輸入,渲染新的內(nèi)容凿滤,所以妈橄,最簡(jiǎn)單的寫法就是前面代碼所示那樣。

EditorState 包括的內(nèi)容大致如下:
(1) 當(dāng)前文本內(nèi)容狀態(tài)(ContentState)
(2) 當(dāng)前選中內(nèi)容狀態(tài)(SelectionState)
(3) 所有的內(nèi)容修飾器(Decorator)
(4) 撤銷和重做棧
(5) 最后一次變更操作的類型翁脆。
Draft.js 提供 covertToRaw 方法可以將 EditorState 對(duì)象轉(zhuǎn)化為 plain JavaScript 對(duì)象眷蚓,從而可以將這些數(shù)據(jù)上傳到后臺(tái),并提供 convertFromRaw 方法將 plain JavaScript 對(duì)象轉(zhuǎn)化為 EditorState 對(duì)象反番。那么轉(zhuǎn)化成的 plain JavaScript 對(duì)象是保存了什么東西呢沙热?
舉個(gè)例子,現(xiàn)在Draft.js編輯器的內(nèi)容如下:


Snip20181116_195.png

那么經(jīng)過 covertToRaw 轉(zhuǎn)換的 plain JavaScript 對(duì)象打印如下:


Snip20181116_196.png

可以看到恬口,這個(gè) plain JavaScript 對(duì)象包含兩個(gè)字段 blocks 和 entityMap校读,各自保存著一個(gè)數(shù)組。其中blocks數(shù)組有7個(gè)元素祖能,每個(gè)元素都描述著當(dāng)前內(nèi)容的一個(gè)塊級(jí)元素歉秫,當(dāng)前內(nèi)容有4行文字,一張圖片养铸,2行空白行(圖片的前后是兩行空白行雁芙,這是Draft.js添加圖片,視頻等資源時(shí)默認(rèn)生成的空白行)钞螟,展開blocks數(shù)組下標(biāo)為0和1兩個(gè)元素如下:
Snip20181116_197.png

text表示該塊級(jí)元素中的純文本兔甘,type 表示該塊級(jí)元素的類型,header-one 表示一級(jí)標(biāo)題鳞滨、unstyled 表示普通段落洞焙、atomic 表示多媒體類的塊級(jí)元素,這些類型,可以直接是庫(kù)提供的澡匪,也可以自定義熔任。庫(kù)提供的類型如下:
Snip20181116_186.png

展開blocks數(shù)組下標(biāo)為2和3的兩個(gè)元素如下:
Snip20181116_198.png

會(huì)發(fā)現(xiàn)下標(biāo)為2的元素的data字段是有值的,該字段表示塊級(jí)元素的樣式(可以是自定義的樣式)唁情,比如我這一行的樣式疑苔,就設(shè)置為了字間距為4px,行間距是2甸鸟,縮進(jìn)2個(gè)字符惦费,對(duì)齊樣式為默認(rèn)(左對(duì)齊)。下標(biāo)為3的元素的inlineStyleRanges字段存儲(chǔ)的數(shù)組有2個(gè)元素抢韭,描述著該行的行內(nèi)樣式薪贫,比如 0: {offset: 0, length: 5, style: "color-rgb(223, 41, 41)"} 表示該塊級(jí)元素的文本,從下標(biāo)為0的文字開始篮绰,長(zhǎng)度為5的字符串的顏色為color-rgb(223, 41, 41)后雷;entityRanges字段存儲(chǔ)的是超鏈接、圖片吠各、視頻等多媒體資源的信息臀突,比如現(xiàn)在“功”這個(gè)字添加了超鏈接,那么entityRanges 對(duì)應(yīng)的數(shù)組的第一個(gè)元素是0: {offset: 4, length: 1, key: 0}贾漏,就表示下標(biāo)為4候学,長(zhǎng)度為1的字符串關(guān)聯(lián)著一個(gè)多媒體資源,而這個(gè)資源的具體數(shù)據(jù)纵散,存儲(chǔ)在entityMap數(shù)組中梳码,這個(gè)key就是用來索引到entityMap數(shù)組中的資源的。blocks數(shù)組下標(biāo)為5的元素描述一張圖片(4和6下標(biāo)的元素是圖片兩個(gè)前后空白行)伍掀,展開如下:
Snip20181116_200.png

entityRanges展開如下:
Snip20181116_201.png

根據(jù)key就能在entityRanges數(shù)組中找到對(duì)應(yīng)位置的資源掰茶。其中,data字段是資源的鏈接等信息蜜笤,mutability分為"MUTABLE"濒蒋,"IMMUTABLE","Segmented"把兔,該字段是用來表示對(duì)應(yīng)著 entity 的文本將如何被修改/刪除沪伙;"MUTABLE"表示對(duì)于的文本在鏈接資源后是可以任意的更改的,"IMMUTABLE"表示對(duì)于的文本鏈接資源后不能隨意更改县好,一旦更改鏈接就將失效围橡。type表示資源的類型,可以為"LINK"缕贡,"IMAGE"翁授,"AUDIO"拣播,"VIDEO"。

由此收擦,知道了 Draft.js 是通過json數(shù)據(jù)來存儲(chǔ)富文本數(shù)據(jù)的诫尽,和傳統(tǒng)的使用html文本存儲(chǔ)符文文本相比大概有以下幾點(diǎn)好處:
(1)更容易取出富文本里面的信息。比如圖片炬守,如果用html文本存儲(chǔ),需要寫復(fù)雜的正則表達(dá)式去匹配圖片的url剂跟,寬高减途,才能取到這些信息。
(2)多端復(fù)用曹洽。json存儲(chǔ)的數(shù)據(jù)鳍置,app將更容易解析出來用原生渲染,而html由于寫法的不統(tǒng)一送淆,有時(shí)候很難保證渲染細(xì)節(jié)的正確性税产。
(3)更加靈活的使用巴拉巴拉。

  1. 自定義塊樣式偷崩,行內(nèi)樣式
    Draft.js 提供了豐富的接口讓開發(fā)者高度定制自己的編輯器辟拷,例如像我這樣基于antd組件開發(fā)的編輯器界面如下:


    Snip20181117_202.png

    上面的一排按鈕就是使用antd組件創(chuàng)建的,基本的思路是點(diǎn)擊按鈕或者其他操作的時(shí)候創(chuàng)建一個(gè)新的editorState阐斜,再賦值給Editor組件衫冻,就改變了內(nèi)容的狀態(tài)。比如下面的一系列塊類型是系統(tǒng)提供的塊類型:


    Snip20181117_203.png

    我點(diǎn)擊其中一種類型谒出,改變光標(biāo)所在行的塊類型隅俘,代碼片段如下:
// 塊類型
const blockTypes = [
    { label: '普通', style: 'unstyled' },
    { label: 'h1', style: 'header-one' },
    { label: 'h2', style: 'header-two' },
    { label: 'h3', style: 'header-three' },
    { label: 'h4', style: 'header-four' },
    { label: 'h5', style: 'header-five' },
    { label: 'h6', style: 'header-six' },
    { label: '引用', style: 'blockquote' },
    { label: '代碼', style: 'code-block' },
    // { label: 'atomic', style: 'atomic' },這個(gè)有問題
    { label: '有序列表', style: 'ordered-list-item' },
    { label: '無序列表', style: 'unordered-list-item' },
]

    // 點(diǎn)擊菜單
    clickMenu = (e) => {
        const newEditState = RichUtils.toggleBlockType(
            this.props.editorState,
            e.key // unstyled header-one header-two ... blockquote code-block ordered-list-item unordered-list-item ...
        )
        this.props.onBlockTypeChange(newEditState)
    }

通過toggleBlockType函數(shù),傳入上一個(gè)editorState和系統(tǒng)塊類型的key笤喳,返回一個(gè)新的editorState为居。
當(dāng)光標(biāo)位置改變時(shí),需要獲取到當(dāng)前光標(biāo)所在行的塊類型杀狡,改變按鈕的文字蒙畴,代碼如下:

// 得到當(dāng)前塊樣式的label
    getCurrentBlockLabel = () => {
        const editorState = this.props.editorState
        const selection = editorState.getSelection()
        const blockStyle = editorState.getCurrentContent().getBlockForKey(selection.getStartKey()).getType()
        let blockLabel = ''
        blockTypes.forEach((blockType) => {
            if (blockType.style === blockStyle) {
                blockLabel = blockType.label
                return
            }
        })
        return blockLabel
    }

使用系統(tǒng)的行內(nèi)樣式,也是差不多的邏輯:

// 行內(nèi)樣式
const inlineTypes = [
    { label: '加粗', style: 'BOLD' },
    { label: '傾斜', style: 'ITALIC' },
    { label: '下劃線', style: 'UNDERLINE' },
    { label: '刪除線', style: 'STRIKETHROUGH' },
]

// 點(diǎn)擊按鈕
    clickBtn = (e, style) => {
        // 阻止點(diǎn)擊按鈕后editor失去了焦點(diǎn)捣卤,而且按鈕的事件必須是onMouseDown忍抽,onClick調(diào)用該方法editor還是會(huì)失去焦點(diǎn)
        e.preventDefault()
        const newEditState = RichUtils.toggleInlineStyle(
            this.props.editorState,
            style
        )
        this.props.onInlineTypeChange(newEditState)
    }

調(diào)用 toggleInlineStyle 函數(shù),需要注意的是在點(diǎn)擊按鈕事件需要使用 onMouseDown 董朝,并且在觸發(fā)的函數(shù)里開頭需要寫 e.preventDefault()鸠项,這樣可以阻止按鈕獲取到焦點(diǎn),光標(biāo)依然保持選中文本的狀態(tài)子姜。
自定義行內(nèi)樣式祟绊,調(diào)用的是 toggleCustomInlineStyle 函數(shù)比如自定義字體大小楼入,文本顏色,代碼如下:

// 點(diǎn)擊菜單
    clickMenu = (e) => {
        
        const newEditState = toggleCustomInlineStyle(
            this.props.editorState,
            'fontSize',
            Number(e.key),
          )
        this.props.onFontSizeChange(newEditState)
    }

// 顏色選擇器選擇的顏色改變牧抽,draft.js不支持更改文字透明度
    handleChangeComplete = (color) => {
        const newTextColor = `rgb(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b})`
        this.setState({ textColor: newTextColor})
        const newEditState = toggleCustomInlineStyle(
            this.props.editorState,
            'color',
            newTextColor,
          )
        this.props.onTextColorChange(newEditState)
    }

改變文字的透明度貌似是不支持的嘉熊,也可能是我姿勢(shì)不對(duì)??。
自定義塊樣式就稍微復(fù)雜點(diǎn)扬舒,分為2步:
(1)塊樣式是存儲(chǔ)在上文說過的data字段中的阐肤,像這個(gè):


Snip20181117_206.png

那么就是往data塞入你想添加的塊樣式。
(2)根據(jù)data中的塊樣式渲染文本內(nèi)容讲坎。需要實(shí)現(xiàn) blockStyleFn 函數(shù)孕惜,如下圖:


Snip20181117_207.png

所以代碼也是分2步走,第一步晨炕,構(gòu)建data字段中的數(shù)據(jù)衫画,需要注意的是當(dāng)你添加一個(gè)塊樣式的時(shí)候,原先的塊樣式會(huì)被完全替換瓮栗,所以需要記錄下之前所有的塊樣式削罩,再在此基礎(chǔ)上添加新的塊樣式,在賦值回去费奸。例如現(xiàn)在添加縮進(jìn):
// 點(diǎn)擊縮進(jìn)按鈕
    onHandleIndentation = (e) => {
        e.preventDefault()

        const { editorState } = this.props
        const selectedBlocksMetadata = getSelectedBlocksMetadata(editorState)
        let newEditorState = null

        if (selectedBlocksMetadata.get('text-indent')) {
            const types = this.getAllBlockType(undefined, selectedBlocksMetadata.get('line-height'), selectedBlocksMetadata.get('letter-spacing'), selectedBlocksMetadata.get('text-align'))
            newEditorState = setBlockData(editorState, types)
        } else {
            const types = this.getAllBlockType('2em', selectedBlocksMetadata.get('line-height'), selectedBlocksMetadata.get('letter-spacing'), selectedBlocksMetadata.get('text-align'))
            newEditorState = setBlockData(editorState, types)
        }

        this.props.onBlockStyleChange(newEditorState)
    }

// 得到總樣式
    getAllBlockType = (textIndent, lineHeight, letterSpacing, textAlign) => {
        return {
            'text-indent': textIndent,
            'line-height': lineHeight,
            'letter-spacing': letterSpacing,
            'text-align': textAlign
        }
    }

接下來實(shí)現(xiàn) myBlockStyleFn 函數(shù)弥激。取出data,動(dòng)態(tài)創(chuàng)建一個(gè)css樣式并返回:

// 自定義樣式匹配
    myBlockStyleFn = contentBlock => {
        const type = contentBlock.getType()
        const metaData = contentBlock.getData()

        const textIndent = metaData.get('text-indent')
        const lineHeight = metaData.get('line-height')
        const letterSpacing = metaData.get('letter-spacing')
        const textAlign = metaData.get('text-align')

        if (textIndent || lineHeight || letterSpacing || textAlign) {
            let letterSpacingName = ''
            if (!letterSpacing) {
                letterSpacingName = letterSpacing
            } else {
                letterSpacingName = Math.round(
                    Number(
                        letterSpacing.substring(0, letterSpacing.indexOf('px'))
                    ) * 100
                ).toString()
            }

            const className =
                'custom' +
                textIndent +
                Math.round(lineHeight * 100) +
                letterSpacingName +
                textAlign
            const { dymanicCssList } = this.state
            let classIsExist = false

            for (const dymanicCss of dymanicCssList) {
                if (dymanicCss === className) {
                    classIsExist = true
                    break
                }
            }

            if (!classIsExist) {
                // console.log(className,textIndent,lineHeight,letterSpacing)
                dymanicCssList.push(className)
                this.loadCssCode(`.${className} {
                    text-indent: ${textIndent};
                    line-height: ${lineHeight};
                    letter-spacing: ${letterSpacing};
                    text-align: ${textAlign};
                }`)
            }
            
            return className
        }
    }

// 動(dòng)態(tài)創(chuàng)建css
    loadCssCode = code => {
        const style = document.createElement('style')
        style.type = 'text/css'
        // style.rel = 'stylesheet';
        // for Chrome Firefox Opera Safari
        style.appendChild(document.createTextNode(code))
        // for IE
        // style.styleSheet.cssText = code;
        const head = document.getElementsByTagName('head')[0]
        head.appendChild(style)
    }

樣式名的創(chuàng)建寫的有些復(fù)雜愿阐,目的就是防止和別的樣式名重復(fù)了秆撮,之前還踩過樣式名存在某些特殊字符的時(shí)候樣式就無效的坑。换况。职辨。,新創(chuàng)建的樣式名會(huì)放入一個(gè)數(shù)組中戈二,下次創(chuàng)建的時(shí)候判斷數(shù)組里面有沒有同名的樣式舒裤,如果存在就不重復(fù)創(chuàng)建了。因?yàn)檫@個(gè) myBlockStyleFn 函數(shù)是會(huì)頻繁調(diào)用的觉吭,基本上你只要改變富文本的任何一個(gè)狀態(tài)(例如光標(biāo)位置改變腾供,添加一個(gè)文字)就會(huì)調(diào)用,其他賦值給Editor的函數(shù)也是同理鲜滩,所以如果你在函數(shù)里的實(shí)現(xiàn)比較耗時(shí)伴鳖,就會(huì)導(dǎo)致你在編輯器中快速添加文字的時(shí)候產(chǎn)生延遲。

3.使用 Entity 對(duì)象創(chuàng)建超鏈接
Entity 是 Draft.js 中用于存儲(chǔ)元數(shù)據(jù)的概念徙硅,所以可以用來表示超鏈接榜聂、圖片、視頻等需要額外數(shù)據(jù)項(xiàng)的多媒體內(nèi)容嗓蘑。該對(duì)象有三個(gè)屬性:
(1)用于表示該 Entity 類型的 type须肆,比如可以取值為 link匿乃、image。
(2)根據(jù) Entity 是否可變豌汇,mutability 具有三種取值:IMMUTABLE幢炸、MUTABLE 和 SEGMENTED。
(3)用于存儲(chǔ) Entity 元數(shù)據(jù)的 data 字段拒贱,比如對(duì)于超鏈接 Entity宛徊,應(yīng)該有一個(gè) href 值。
例如逻澳,現(xiàn)在我選中一段文字岩调,點(diǎn)擊添加鏈接按鈕為其添加超鏈接:


image.png

首先需要獲取到選中的文字,然后根據(jù)鏈接創(chuàng)建一個(gè)entity對(duì)象赡盘,再將選中文字和entity對(duì)象綁定,再創(chuàng)建新的editorState缰揪,代碼如下:

// 得到editorState的title
    getBeginTitle = (editorState) => {
        const selectionState = editorState.getSelection()
        const anchorKey = selectionState.getAnchorKey()
        const currentContent = editorState.getCurrentContent()
        const currentContentBlock = currentContent.getBlockForKey(anchorKey)
        const start = selectionState.getStartOffset()
        const end = selectionState.getEndOffset()
        const title = currentContentBlock.getText().slice(start, end)
        return title
    }

// 點(diǎn)擊確認(rèn)按鈕
    handleOk = (e) => {
        e.preventDefault()
        
        // 參考wysiwyg
        const { title, editorUrl } = this.state
        const { editorState } = this.props
        const selection = editorState.getSelection()
        const entityKey = editorState
            .getCurrentContent()
            .createEntity('LINK', 'MUTABLE', { url: editorUrl })
            .getLastCreatedEntityKey()
        const contentState = Modifier.replaceText(
            editorState.getCurrentContent(),
            selection,
            `${title}`,
            editorState.getCurrentInlineStyle(),
            entityKey,
        )
        const newEditorState = EditorState.push(editorState, contentState, 'insert-characters')
        this.props.onAddLink(newEditorState)
        this.setState({
            visible: false,
            title: '',
            editorUrl: ''
        })
    }
  1. 自定義塊級(jí)元素的渲染
    Draft.js允許開發(fā)者自己實(shí)現(xiàn)塊級(jí)元素的渲染陨享,只要實(shí)現(xiàn) blockRendererFn 函數(shù)。例如現(xiàn)在我要往富文本中加入一張圖片钝腺,然后用img標(biāo)簽抛姑,左對(duì)齊顯示這張圖片,如圖:


    Snip20181117_210.png
// 點(diǎn)擊確定按鈕
    handleOk = e => {
        e.preventDefault()
        const { editorState } = this.props
        const { url, width, height } = this.state
        const contentState = editorState.getCurrentContent()
        const contentStateWithEntity = contentState.createEntity(
            'IMAGE',
            'IMMUTABLE',
            {
                src: url,
                width,
                height
            }
        )
        const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
        const newEditorState = EditorState.set(editorState, {
            currentContent: contentStateWithEntity
        })

        const newNewEditorState = AtomicBlockUtils.insertAtomicBlock(
            newEditorState,
            entityKey,
            ' '
        )
        this.props.onAddImage(newNewEditorState)
    }

然后在實(shí)現(xiàn) blockRendererFn 函數(shù)艳狐,該函數(shù)接受一個(gè)block定硝,判斷block是否為atomic類型,如果是毫目,使用自定義組件渲染:

// image,mp3,mp4的渲染組件匹配
    mediaBlockRenderer = block => {
        if (block.getType() === 'atomic') {
            return {
                component: Media,
                editable: false
            }
        }
        return null
    }

const Audio = (props) => {
    return <audio controls src={props.src} style={{ width: '100%', whiteSpace: 'initial' }} />
}
const Image = (props) => {

    return <div style={{textAlign:'left'}}><img src={props.src} style={{ width: props.width, height: props.height,whiteSpace: 'initial'}} /></div>
}
const Video = (props) => {
    return <video controls src={props.src} style={{ width: '100%', whiteSpace: 'initial' }} />
}
const Media = (props) => {

    const key = props.block.getEntityAt(0)

    if (key) {
        const entity = props.contentState.getEntity(
            key
        );
        const { src } = entity.getData()
        const type = entity.getType()
        let media
        if (type === 'audio') {
            media = <Audio src={src} />
        } else if (type === 'IMAGE') {
            const { width, height } = entity.getData()
            media = <Image src={src} width={width} height={height} />
        } else if (type === 'video') {
            media = <Video src={src} />
        }
        return media
    }
    
    return null
};

需要注意的是蔬啡,這里需要實(shí)現(xiàn) handleKeyCommand 函數(shù),處理鍵盤事件镀虐,否則你使用鍵盤的delete 鍵刪除圖片時(shí)箱蟆,只是將圖片的塊級(jí)元素刪除掉,entityMap數(shù)組里依然保存著這張圖片的數(shù)據(jù):


Snip20181117_211.png
handleKeyCommand = (command, editorState) => {
        const newState = RichUtils.handleKeyCommand(editorState, command)
        if (newState) {
            this.onEditorStateChange(newState)
            return true
        }
        return false
    }
  1. 自定義行內(nèi)元素的渲染
    Draft.js使用裝飾器 Decorator 來渲染行內(nèi)元素刮便,比如對(duì)于上面的超鏈接元素空猜,則需要如下的代碼將其渲染成一個(gè) Link 組件:
/ 自定義組件,用于超鏈接
const Link = (props) => {
    // 這里通過contentState來獲取entity?恨旱,之后通過getData獲取entity中包含的數(shù)據(jù)
    const { url } = props.contentState.getEntity(props.entityKey).getData();
    return (
        <a href={url}>
            {props.children}
        </a>
    )
}

// decorator辈毯,用于超鏈接
const decorator = new CompositeDecorator([
    {
        strategy (contentBlock, callback, contentState) {

            // 這個(gè)方法接收2個(gè)函數(shù)作為參數(shù),如果第一個(gè)參數(shù)的函數(shù)執(zhí)行時(shí)?返回true搜贤,就會(huì)執(zhí)行第二個(gè)參數(shù)函數(shù)谆沃,同時(shí)會(huì)?將匹配的?字符的起始位置和結(jié)束位置傳遞給第二個(gè)參數(shù)。
            contentBlock.findEntityRanges(
                (character) => {
                    const entityKey = character.getEntity();
                    return (
                        entityKey !== null &&
                        contentState.getEntity(entityKey).getType() === 'LINK'
                    );
                }, (...arr) => {
                    callback(...arr)
                }
            );
        },
        component: Link
    }
]);

然后在初始化 editorState 的時(shí)候傳入 decorator:

state = {
        editorState: EditorState.createEmpty(decorator)
    }
  1. editorState plainObject html字符串的相互轉(zhuǎn)化
    有時(shí)候使用Draft.js生成的富文本可能需要轉(zhuǎn)化為html字符串仪芒,官方只提供editorState與plainObject的相互轉(zhuǎn)化管毙,不提供editorState與html的相互轉(zhuǎn)化腿椎。不過已經(jīng)有人將plainObject轉(zhuǎn)html這一層寫好了,github鏈接:https://github.com/jpuri/draftjs-to-html夭咬。也能將html轉(zhuǎn)化為editorState啃炸。github鏈接:https://github.com/jpuri/html-to-draftjs。這兩個(gè)工具都是同一個(gè)作者卓舵,是為作者寫的富文本編輯器服務(wù)的:https://github.com/jpuri/react-draft-wysiwyg南用。實(shí)際測(cè)試的時(shí)候發(fā)現(xiàn),如果是你自定義的樣式很有可能使用上面兩個(gè)工具在html和editorState相互轉(zhuǎn)化會(huì)失敗√屯澹現(xiàn)在我的解決方案是將 plainObject 轉(zhuǎn)化成 json 字符串裹虫,利用 draftjs-to-html 將 plainObject 轉(zhuǎn) html 字符串,將兩種字符串都傳遞給后臺(tái)融击,這樣使用Draft.js 編輯的富文本可以轉(zhuǎn)化為 html 顯示筑公,而使用Draft.js編輯時(shí)也能取到j(luò)son字符串轉(zhuǎn)化為editorState顯示。

  2. 自定義的富文本編輯器github鏈接:https://github.com/linzhesheng/YdjRichEditor尊浪。

  3. 參考文章:
    Draft.js文檔
    使用 Draft.js 來構(gòu)建一個(gè)現(xiàn)代化的編輯器
    draft.js在知乎的實(shí)踐

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末匣屡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拇涤,更是在濱河造成了極大的恐慌捣作,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹅士,死亡現(xiàn)場(chǎng)離奇詭異券躁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)掉盅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門也拜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人趾痘,你說我怎么就攤上這事搪泳。” “怎么了扼脐?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵耗溜,是天一觀的道長(zhǎng)褪测。 經(jīng)常有香客問我诫欠,道長(zhǎng)意述,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任肚吏,我火速辦了婚禮方妖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘罚攀。我一直安慰自己党觅,他們只是感情好雌澄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杯瞻,像睡著了一般镐牺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上魁莉,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天睬涧,我揣著相機(jī)與錄音,去河邊找鬼旗唁。 笑死畦浓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的检疫。 我是一名探鬼主播讶请,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼屎媳!你這毒婦竟也來了夺溢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤剿牺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后环壤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晒来,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年郑现,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了湃崩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡接箫,死狀恐怖攒读,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辛友,我是刑警寧澤薄扁,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站废累,受9級(jí)特大地震影響邓梅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜邑滨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一日缨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掖看,春花似錦匣距、人聲如沸面哥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尚卫。三九已至,卻和暖如春恩静,著一層夾襖步出監(jiān)牢的瞬間焕毫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工驶乾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留邑飒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓级乐,卻偏偏與公主長(zhǎng)得像疙咸,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子风科,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • Draft.js是Facebook開源的開發(fā)React富文本編輯器開發(fā)框架撒轮。和其它富文本編輯器不同,draft.j...
    MarxJiao閱讀 14,608評(píng)論 0 13
  • 1贼穆、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽明先生_X自主閱讀 15,979評(píng)論 3 119
  • 小學(xué)和初中题山,總是讓寫記敘文,閱讀題也讓分析記敘文用什么手法表達(dá)什么情感之類的故痊。近日顶瞳,人民日?qǐng)?bào)微博批評(píng)小學(xué)生認(rèn)為家長(zhǎng)...
    冷無常閱讀 722評(píng)論 3 7
  • 我想我作 是因?yàn)槲矣X得你給我的安全感不夠 我慌了 所以我總想找點(diǎn)事情證明你愛我 我不知道你為什么總在上班總在代課 ...
    鸗鸑閱讀 285評(píng)論 0 0
  • 百日練,一百天看一百本書愕秫,第80天慨菱,《黃金時(shí)代》30分鐘,理解70%戴甩,2800字/分鐘 講到劉老先生為了吃烤鴨而死...
    騎了蝸牛闖世界閱讀 206評(píng)論 0 0