寫寫文章總結(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ǔ)富文本的方式碘菜。
- 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)容如下:
那么經(jīng)過 covertToRaw 轉(zhuǎn)換的 plain JavaScript 對(duì)象打印如下:
可以看到恬口,這個(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è)元素如下:
text表示該塊級(jí)元素中的純文本兔甘,type 表示該塊級(jí)元素的類型,header-one 表示一級(jí)標(biāo)題鳞滨、unstyled 表示普通段落洞焙、atomic 表示多媒體類的塊級(jí)元素,這些類型,可以直接是庫(kù)提供的澡匪,也可以自定義熔任。庫(kù)提供的類型如下:
展開blocks數(shù)組下標(biāo)為2和3的兩個(gè)元素如下:
會(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è)前后空白行)伍掀,展開如下:
entityRanges展開如下:
根據(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)更加靈活的使用巴拉巴拉。
-
自定義塊樣式偷崩,行內(nèi)樣式
Draft.js 提供了豐富的接口讓開發(fā)者高度定制自己的編輯器辟拷,例如像我這樣基于antd組件開發(fā)的編輯器界面如下:
上面的一排按鈕就是使用antd組件創(chuàng)建的,基本的思路是點(diǎn)擊按鈕或者其他操作的時(shí)候創(chuàng)建一個(gè)新的editorState阐斜,再賦值給Editor組件衫冻,就改變了內(nèi)容的狀態(tài)。比如下面的一系列塊類型是系統(tǒng)提供的塊類型:
我點(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è):
那么就是往data塞入你想添加的塊樣式。
(2)根據(jù)data中的塊樣式渲染文本內(nèi)容讲坎。需要實(shí)現(xiàn) blockStyleFn 函數(shù)孕惜,如下圖:
所以代碼也是分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)擊添加鏈接按鈕為其添加超鏈接:
首先需要獲取到選中的文字,然后根據(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: ''
})
}
-
自定義塊級(jí)元素的渲染
Draft.js允許開發(fā)者自己實(shí)現(xiàn)塊級(jí)元素的渲染陨享,只要實(shí)現(xiàn) blockRendererFn 函數(shù)。例如現(xiàn)在我要往富文本中加入一張圖片钝腺,然后用img標(biāo)簽抛姑,左對(duì)齊顯示這張圖片,如圖:
// 點(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ù):
handleKeyCommand = (command, editorState) => {
const newState = RichUtils.handleKeyCommand(editorState, command)
if (newState) {
this.onEditorStateChange(newState)
return true
}
return false
}
- 自定義行內(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)
}
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顯示。自定義的富文本編輯器github鏈接:https://github.com/linzhesheng/YdjRichEditor尊浪。
參考文章:
Draft.js文檔
使用 Draft.js 來構(gòu)建一個(gè)現(xiàn)代化的編輯器
draft.js在知乎的實(shí)踐