slatejs实践

简介


Slate.js 是一个完全可定制的的富文本编辑器,准确来说是一个框架。其诞生于 2016 年,作者是 Ian Storm Taylor。它和 Draft.js, Prosemirror, Quill类似,都是基于结构化对象来渲染富文本内容。

slate.js 架构设计类似于 MVC

  • (Model)slate 定义了一套数据模型以及更新 model 的一系列 commonds。
  • (View) slate 定义了一套与数据模型对应的视图模型(洋葱模型),使用 react 将数据模型渲染成视图模型。
  • (Ctrl) slate 支持自定义事件监听,然后通过 commonds 调用更新数据模型。

这里,commonds指的是 slate 内部定义的一系列 Operation。和用来生产Operation的一系列Transforms 辅助方法。
model 更新以后会通过一些规则来保证数据格式的规范,对model进行正确性校验,然后触发view变更。

slate 仓库下包含四个 package

  • slate:这一部分是编辑器的核心,定义了数据模型(model),操作模型的方法和编辑器实例本身。
  • slate-react:以插件的形式提供 DOM 渲染和用户交互能力,包括光标、快捷键等等。
  • slate-history:以插件的形式提供 undo/redo 能力。
  • slate-hyperscript:让用户能够使用 JSX语法来创建 slate 的数据。

特点


  • 灵活,完全可定制。
  • 插件是一等公民,你可以以插件的形式定制自己的用于修改编辑器的行为API.
  • 数据模型类似于可嵌套的Dom树,Schema结构非常精简。
  • 具有原子化操作 API,支持协同编辑。
  • 使用 React 作为渲染层;

slate 数据模型


slate 以树形结构来表示和存储文档内容,树的节点类型为 Node,分为三种子类型:

export type Node = Editor | Element | Text

export interface Element {
  children: Node[]
  [key: string]: unknown
}

export interface Text {
  text: string
  [key: string]: unknown
}
  • Editor 是一种特殊的 Element ,它既是编辑器实例类型,也是文档树的根节点
  • Element 类型含有 children 属性,可以作为其他 Node 的父节点
  • Text 类型是树的叶子结点,包含文字信息

用户可以自行拓展 Node 的属性,例如通过添加 type 字段标识Node 的类型(paragraph, ordered list, heading 等等),或者是文本的属性(italic, bold等等),来描述富文本中的文字和段落。

const initialValue = [
    {
        type: 'paragraph',
        children: [{ text: '我是', bold: true }, { text: '一行', underline: true }, {text: '文字'}]
    },
    {
        type: 'code',
        children: [{ text: 'hello world' }]
    },
    {
        type: 'image',
        children: [],
        url: 'xxx.png'
    }
    // 其他的继续扩展
]

slate 渲染模型


slate数据模型通过slate-react视图渲染以后的组件层级如下图所示:

你会看到一些 data- 开头的自定义内置特性(attribute),比如 data-slate-node 等。

slate主要内置特性如下:

  • Editable

    • data-slate-editor 用于标识编辑器组件。
  • Element

    • data-slate-node: ‘element’|’value’|’text’;取值分别代表元素、文档全量值(适用于 Editable 上)、文本节点。
    • data-slate-void: 若为空元素则取值为 true,否则不存在。
    • data-slate-inline: 若为内联元素则取值为 true,否则不存在。
  • Leaf

    • data-slate-leaf: 必须,取值为 true,表明对应 DOM 元素为 Leaf 节点。
  • String

    • data-slate-string: 若为文本节点则取值为 true,否则不存在。
    • data-slate-zero-width: 若为零宽度文本节点则取值 ‘n’|’z’,分别指代换行、不换行,否则不存在。
    • data-slate-length: 用于标注零宽度文本节点的实际宽度,单位为字符数。默认为 0,如果不为零则为被设置了 isVoid 的元素的文本字符的宽度。

此外,对于 Elementattributes 中还有以下内置特性内容:

  • contentEditable: 若不可编辑则取值为 false,否则不存在。
  • dir: 若编辑方向为从右到左则取值 ‘rtl’,否则不存在。
  • ref: 必选,当前元素的 ref 引用。Slate 会在每次 Element 渲染时将该元素和其对应 DOM 节点的映射关系添加到 ELEMENT_TO_NODE 的 WeakMap 中。若缺少 ref 则会因为 ELEMENT_TO_NODE 中映射关系的缺失而导致渲染失败和 toSlateNode 中报错。

简单实例



import React, { useState, useMemo } from 'react'
import { createEditor } from 'slate'  // 创建编辑器实例的方法
import { Slate, Editable, withReact } from 'slate-react'
import { withHistory } from 'slate-history'; //以插件的形式提供 undo/redo 能力

// 初始化编辑器内容的数据。其结构类似于 vnode。
const initialValue = [
  {
    type: 'paragraph',
    children: [ { text: '我是一行文字' } ]
  }
]

export default  BasicEditor = () => {
    /** editor 变量为编辑器的对象实例,可以使用它提供的大量 API,也可以用来扩展其他插件。 */
    const editor = useMemo(() => withHistory(withReact(createEditor())) ,[])
    const [value, setValue] = useState(initialValue)

    return (
        <div style={{ border: '1px solid #ccc', padding: '10px' }}>
            <Slate
                editor={editor}
                onChange={newValue => setValue(newValue)}
            >
                <Editable/>
            </Slate>
        </div>
    )
}

renderElement

slate.js 提供了 renderElement 让我们来自定义渲染逻辑,不过先别着急。富文本编辑器嘛,肯定不仅仅只有文字,还有很多数据类型,这些都是需要渲染的,所以都要依赖于这个 renderElement 。

renderLeaf

renderElement 是渲染 Element ,但是它管不了更底层的 Text ,所以 slate.js 提供了 renderLeaf ,用来控制文本格式。renderElement 和 renderLeaf 并不冲突,可以一起用。

自定义事件

在 slate.js,自定义事件可以直接在 组件监听 DOM 事件即可。通常,一些快捷键可以通过这种方式来设置。

操作API

到 Editor 和 Transforms 对象里封装了很多常用的一系列 API 。可以增加样式,操作节点等。

简单插件

slate.js 提供的是编辑器的基本能力,如果不能满足使用,它提供了插件机制供用户去自行扩展。
另外,有了规范的插件机制,还可以形成自己的社区,可以直接下载使用第三方插件。

插件开发其实很简单,就是对 editor 的扩展和装饰。你想要做什么,可以充分返回自己的想象力。

数据处理

其他概念

  • contenteditable
  • execCommand
  • Selection 和 Range
  • 自定义组件

参考文献