Slate.js初始化HTML以及添加图片插件的问题

刚好需要一款富文本编辑器,在对比了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里; 图片展示的时候不仅仅需要渲染图片,还有管理编辑功能,比如图片删除,图片展示样式修改等。