wangEditor5原理解析

579 阅读3分钟

背景了解

【磨剑一年】富文本编辑器 wangEditor V5 正式发布
这篇文章是wangEditor作者写的,里面链接了很多文章,可以对wangEditor5的历程和依赖项有很好的了解。

架构图

image.png

  • wangEditor 是基于slate.js为内核开发的;
  • 使用 vdom 技术(基于 snabbdom.js )做视图更新,model 和 view 分离,增加稳定性;

slate的json格式

image.png 以下是我在wangEditor的demo编辑器上设置了各种格式,生成的slate json结构:

{
    "children": [
        {
            "type": "header1",
            "children": [
                {
                    "text": "Good lucky to you~"
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "text": "Good lucky to you~"
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "text": "hello~",
                    "code": true
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "text": "2",
                    "fontSize": "22px"
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "text": "wow haha",
                    "fontFamily": "华文楷体"
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "text": "lineheight"
                }
            ],
            "lineHeight": "3"
        },
        {
            "type": "list-item",
            "lineHeight": "3",
            "children": [
                {
                    "text": "wawawa"
                }
            ],
            "ordered": false
        },
        {
            "type": "list-item",
            "lineHeight": "3",
            "ordered": false,
            "children": [
                {
                    "text": "hahaha"
                }
            ]
        },
        {
            "type": "list-item",
            "lineHeight": "3",
            "children": [
                {
                    "text": "nihaoma"
                }
            ],
            "ordered": true
        },
        {
            "type": "list-item",
            "lineHeight": "3",
            "ordered": true,
            "children": [
                {
                    "text": "wasai"
                }
            ]
        },
        {
            "type": "todo",
            "lineHeight": "3",
            "children": [
                {
                    "text": "make money"
                }
            ]
        },
        {
            "type": "paragraph",
            "lineHeight": "3",
            "children": [
                {
                    "text": "right 1"
                }
            ],
            "textAlign": "right"
        },
        {
            "type": "paragraph",
            "lineHeight": "3",
            "textAlign": "right",
            "children": [
                {
                    "text": "right 2"
                }
            ]
        },
        {
            "type": "paragraph",
            "lineHeight": "3",
            "textAlign": "left",
            "children": [
                {
                    "text": "suojin"
                }
            ],
            "indent": "2em"
        },
        {
            "type": "paragraph",
            "lineHeight": "3",
            "textAlign": "left",
            "indent": "2em",
            "children": [
                {
                    "text": " "
                },
                {
                    "type": "link",
                    "url": "https://www.baidu.com",
                    "children": [
                        {
                            "text": "who know"
                        }
                    ]
                },
                {
                    "text": " "
                }
            ]
        },
        {
            "type": "paragraph",
            "lineHeight": "3",
            "textAlign": "left",
            "indent": "2em",
            "children": [
                {
                    "text": ""
                }
            ]
        },
        {
            "type": "video",
            "src": "https://www.iqiyi.com/v_1vxy9fa2d74.html?vfrm=pcw_home&vfrmblk=B&vfrmrst=fcs_0_p11",
            "poster": "",
            "children": [
                {
                    "text": ""
                }
            ]
        },
        {
            "type": "table",
            "width": "auto",
            "children": [
                {
                    "type": "table-row",
                    "children": [
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": ""
                                }
                            ],
                            "isHeader": true
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": ""
                                }
                            ],
                            "isHeader": true
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": ""
                                }
                            ],
                            "isHeader": true
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": ""
                                }
                            ],
                            "isHeader": true
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": ""
                                }
                            ],
                            "isHeader": true
                        }
                    ]
                },
                {
                    "type": "table-row",
                    "children": [
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "1"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "1"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "1"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "1"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "1"
                                }
                            ]
                        }
                    ]
                },
                {
                    "type": "table-row",
                    "children": [
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        }
                    ]
                },
                {
                    "type": "table-row",
                    "children": [
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "3"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        }
                    ]
                },
                {
                    "type": "table-row",
                    "children": [
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "4"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        },
                        {
                            "type": "table-cell",
                            "children": [
                                {
                                    "text": "2"
                                }
                            ]
                        }
                    ]
                }
            ]
        },
        {
            "type": "pre",
            "children": [
                {
                    "type": "code",
                    "language": "",
                    "children": [
                        {
                            "text": "const a = 9999999999;"
                        }
                    ]
                }
            ]
        },
        {
            "type": "divider",
            "children": [
                {
                    "text": ""
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "bgColor": "rgb(255, 122, 69)",
                    "text": "Good lucky to you~"
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "text": "Good lucky to you~",
                    "color": "rgb(225, 60, 57)"
                }
            ]
        },
        {
            "type": "blockquote",
            "children": [
                {
                    "text": "Good lucky to you~",
                    "color": "rgb(225, 60, 57)"
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "text": "Good lucky to you~",
                    "color": "rgb(225, 60, 57)",
                    "bold": true
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "text": "Good lucky to you~",
                    "color": "rgb(225, 60, 57)",
                    "italic": true
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "text": "Good lucky to you~",
                    "color": "rgb(225, 60, 57)",
                    "through": true
                }
            ]
        },
        {
            "type": "paragraph",
            "children": [
                {
                    "color": "rgb(225, 60, 57)",
                    "text": "😙😖"
                },
                {
                    "type": "image",
                    "src": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAACUUAAAuBCAYAAADvvh30AAAACXBIWXMAABYlAAAWJQx1/jQd3rqi5......SuQmCC",
                    "href": "",
                    "alt": "前端技能汇总.png",
                    "style": {},
                    "children": [
                        {
                            "text": ""
                        }
                    ]
                },
                {
                    "text": ""
                }
            ]
        }
    ]
}

image.png

JSON 数据就是编辑器的 Model 。可以把编辑器认为是一个黑盒,输入输出的可以是 HTML ,但其内部数据是规定格式的 JSON 数据。

渲染流程

image.png

Model -> DOM

renderElem - 将 JSON 结构渲染为编辑器 DOM(全部是单层内容)

image.png

数据驱动视图,这一点和 Vue React 一样,所以我们也做了类似的设计:第一,把 Model 转换为 vdom ;第二,把 vdom patch 到真实 DOM 中(使用 snabbdom.js)。 这么一对比,这里的 Model 就相当于 Vue 中的 data ,React 中的 state 。
但是还要考虑各个组件渲染为不同的 DOM 类型,所以需要让各个模块注册自己的 Render 函数,再统一渲染。

/**
 * render paragraph elem
 * @param elemNode slate elem
 * @param children children
 * @param editor editor
 * @returns vnode
 */
function renderParagraph(
  elemNode: SlateElement,
  children: VNode[] | null,
  editor: IDomEditor
): VNode {
  const vnode = <p>{children}</p>
  return vnode
}

export const renderParagraphConf = {
  type: 'paragraph',
  renderElem: renderParagraph,
}

Model -> HTML

elemToHtml - 将 JSON 结构转换为 HTML 内容(单层结构 -> 嵌套结构)

image.png

编辑器 Model 生成并输出 HTML ,不同的组件需要生成不同的 HTML 格式。也需要各个模块注册自己的 toHtml 函数,再统一生成 HTML。

function pToHtml(elem: Element, childrenHtml: string): string {
  if (childrenHtml === '') {
    return '<p><br></p>'
  }
  return `<p>${childrenHtml}</p>`
}

export const pToHtmlConf = {
  type: 'paragraph',
  elemToHtml: pToHtml,
}

HTML -> Model

parseElemHtml - 将 HTML 内容转换为 JSON 结构(嵌套结构 -> 单层结构)

image.png

户输入 HTML 转换为 Model (JSON 数据),不同的组件需要生成不同的 JSON 格式。也需要各个模块注册自己的 parseHtml 函数,再统一生成 Model 。

function parseParagraphHtml(
  elem: DOMElement,
  children: Descendant[],
  editor: IDomEditor
): ParagraphElement {
  const $elem = $(elem)

  children = children.filter(child => {
    if (Text.isText(child)) return true
    if (editor.isInline(child)) return true
    return false
  })

  // 无 children ,则用纯文本
  if (children.length === 0) {
    children = [{ text: $elem.text().replace(/\s+/gm, ' ') }]
  }

  return {
    type: 'paragraph',
    // @ts-ignore
    children,
  }
}

export const parseParagraphHtmlConf = {
  selector: 'p:not([data-w-e-type])', // data-w-e-type 属性,留给自定义元素,保证扩展性
  parseElemHtml: parseParagraphHtml,
}

这里严格来说应该是DOM -> Model

整体逻辑

image.png

image.png

image.png

参考