刚好需要一款富文本编辑器,在对比了slate.js和draft.js后,我选择了slate.js。然后自己开发插件来实现图片上传和mention需求。
如果前端展现框架也是React,则可以直接将state转换成JSON后存在即可。但因为我前端直接渲染的是HTML,所以直接将state转换成HTML后存在数据库。但这在后台编辑的时候多了一步,需要将HTML转成成Descendant[]
。
slate提供了一个插件slate-html-serializer将HTML转成成state。但实际运行的时候发现报错了。
网上搜索了下结果,发现基本都是老旧的信息了,无法解决实际问题。只能自己在文档找答案。
在https://docs.slatejs.org/concepts/xx-migrating里才发现,slate-html-deserializer
已经被移除了。
在https://docs.slatejs.org/concepts/10-serializing中可以看到新的格式转换DEMO。
首先,需要通过一个Dom解析器,将HTML字符串解析成Document。在浏览器环境中,使用原生的 DOMParser 来解析 HTML。代码如下:
const html = '...'
const document = new DOMParser().parseFromString(html, 'text/html')
const descendants = deserialize(document.body)
其次,解析出来的是DOM 树
。但Slate接受的是段落 block列表,为此还需要定义一个函数,将Dom数的节点转成Slate定义的Block数组。如DEMO所示:
import { jsx } from 'slate-hyperscript'
const deserialize = (el, markAttributes = {}) => {
if (el.nodeType === Node.TEXT_NODE) {
return jsx('text', markAttributes, el.textContent)
} else if (el.nodeType !== Node.ELEMENT_NODE) {
return null
}
const nodeAttributes = { ...markAttributes }
// define attributes for text nodes
switch (el.nodeName) {
case 'STRONG':
nodeAttributes.bold = true
}
const children = Array.from(el.childNodes)
.map(node => deserialize(node, nodeAttributes))
.flat()
if (children.length === 0) {
children.push(jsx('text', nodeAttributes, ''))
}
switch (el.nodeName) {
case 'BODY':
return jsx('fragment', {}, children)
case 'BR':
return '\n'
case 'BLOCKQUOTE':
return jsx('element', { type: 'quote' }, children)
case 'P':
return jsx('element', { type: 'paragraph' }, children)
case 'A':
return jsx(
'element',
{ type: 'link', url: el.getAttribute('href') },
children
)
default:
return children
}
}
最终,实现代码如下:
const renderElement = useCallback(props => <Element {...props} />, [])
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const html = '...' // 需要解析的HTML代码
const document = new DOMParser().parseFromString(html, 'text/html')
const initialValue = deserialize(document.body)
return (
<Slate editor={editor} initialValue={defaultVallue}>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder='Enter some rich text…'
/>
</Slate>
)
不过,发现文中的图片并没有展示。因为默认没有解析和渲染image标签,为此,需要在deserialize
代码的switch里增加image分支,如:
switch (el.nodeName) {
case 'BODY':
return jsx('fragment', {}, children)
case 'BR':
return '\n'
case 'BLOCKQUOTE':
return jsx('element', {type: 'quote'}, children)
case 'P':
return jsx('element', {type: 'paragraph'}, children)
case 'A':
return jsx('element', {type: 'link', url: el.getAttribute('href')}, children)
case 'IMG':
return jsx('element', {type: 'image', url: el.getAttribute('src')}, children)
default:
return children
}
但此时只是增加了image的解析,如果要展示image,还需要增加一个插件Plugins。文档里关于插件的解释不够详细,不过我们可以参考一个内置的插件slate-history。
import {Transforms} from 'slate'
import {ReactEditor} from 'slate-react'
export type EmptyText = {
text: string
}
export type ImageElement = {
type: 'image'
url: string
children: EmptyText[]
}
export const withImages = <T extends ReactEditor>(editor: T) => {
editor.insertData = data => {
const text = data.getData('text/plain')
insertImage(editor, text)
}
return editor
}
const insertImage = <T extends ReactEditor>(editor: T, url: string) => {
const text = {text: ''}
const image: ImageElement = {type: 'image', url, children: [text]}
Transforms.insertNodes(editor, image)
}
当然,这只是个简单的图片展示插件,实际上图片插件比这复杂,起码在的toolbar区域需要一个图片添加按钮,点击这个按钮会触发图片上传或URL添加,然后将需要添加的图片的url通过insertImage
方法添加到state里; 图片展示的时候不仅仅需要渲染图片,还有管理编辑功能,比如图片删除,图片展示样式修改等。