slate源码解析(四):插件系统

1,138 阅读4分钟

Slate 的插件系统是一种强大的扩展机制,允许你以模块化的方式向编辑器添加功能、样式和行为。它提供了许多钩子函数和工具,使你可以轻松地自定义编辑器的行为,以满足特定的需求。

插件系统的核心思想是使用装饰器模式,通过对 Editor 对象应用装饰器来扩展其功能。Editor 是编辑器的核心对象,它提供了一系列操作和管理编辑器内容的方法。然后我们通过重新编辑 Editor 提供的工具方法,来达到自定义编辑器功能的效果。

Editor对象

我们可以通过提供的 createEditor 来获取到 editor 对象:

const editor = createEditor()

然后通过编写装饰器函数来创建插件,比如官方提供的 withReactwithHistory

在官方例子中:

// site/examples/richtext.tsx
const editor = useMemo(() => withHistory(withReact(createEditor())), [])

每一个with函数,通过接受一个初始 editor 对象,在函数中编辑重写 editor 对象提供的各种工具方法,最后再返回一个修改过后的 editor 对象。

比如编辑editor提供的 insertText 方法,达到我们输入字符为 'a' 的时候,控制台就打印一个 'hello world' :

const myPlugin = (editor: Editor) => {
  const { insertText } = editor
  editor.insertText = (text: string) => {
    if (text === 'a') {
      console.log('hello world')
    }    insertText(text)
  }
  return editor
}
const editor = useMemo(() => myPlugin(withHistory(withReact(createEditor()))), [])

其中我们在insertText中添加了自己需要的操作,最后再调用insertText函数来运行原先的功能。

下面总结了一些 etitor 对象提供的常用的功能模块:

// 用于向选中的文本范围中添加一个标记
addMark: (...args) => addMark(editor, ...args)

// 用于删除操作
deleteBackward: (...args) => deleteBackward(editor, ...args)
deleteForward: (...args) => deleteForward(editor, ...args)
deleteFragment: (...args) => deleteFragment(editor, ...args)

// 用于插入操作
insertBreak: (...args) => insertBreak(editor, ...args)
insertSoftBreak: (...args) => insertSoftBreak(editor, ...args)
insertFragment: (...args) => insertFragment(editor, ...args)
insertNode: (...args) => insertNode(editor, ...args)
insertText: (...args) => insertText(editor, ...args)

// 用于判断编辑器的状态或者选区位置
isBlock: (...args) => isBlock(editor, ...args)
isEdge: (...args) => isEdge(editor, ...args)
isEmpty: (...args) => isEmpty(editor, ...args)
isEnd: (...args) => isEnd(editor, ...args)
isNormalizing: (...args) => isNormalizing(editor, ...args)
isStart: (...args) => isStart(editor, ...args)

// 这是Slate编辑器的核心操作方法,传入对应的 operations 来实现对内容的修改、插入、删除等操作
apply: (...args) => apply(editor, ...args),

// ......

Slate-history解析

在第一章《slate源码解析(一):总体概览》里介绍了结构目录,源码使用了 monorepo 的管理方式,各个包存于 /packages 文件下。其中 Slate-history 就是以插件的形式提供给Slate 历史记录功能的模块,实现了编辑时候撤销和还原的操作。下面我们来对这个官方提供的插件做一个源码解析。

Slate-history 提供了一个 withHistory 的装饰器函数:

export const withHistory = <T extends Editor>(editor: T) => {
  const e = editor as T & HistoryEditor
  const { apply } = e
  e.history = { undos: [], redos: [] }

  e.redo = () => { //... }

  e.undo = () => { //... }

  e.apply = (op: Operation) => {
    // ...
    apply(op)
  }

  e.writeHistory = (stack: 'undos' | 'redos', batch: any) => {
    e.history[stack].push(batch)
  }

  return e
}

从传入参数里获取到上一个函数返回的 editor 对象,然后给 editor 新增了一个 history 对象,其中包括 undos 的操作栈和 redos 的操作栈。新增两个方法,redo 和 undo ,用来实现撤销和还原历史操作栈。修改了 apply 方法,使得每次slate操作时,都会由 history 去记录。

在apply的写入中,有一个合并操作:

const shouldMerge = (op: Operation, prev: Operation | undefined): boolean => {
  if (
    prev &&
    op.type === 'insert_text' &&
    prev.type === 'insert_text' &&
    op.offset === prev.offset + prev.text.length &&
    Path.equals(op.path, prev.path)
  ) {
    return true
  }

  if (
    prev &&
    op.type === 'remove_text' &&
    prev.type === 'remove_text' &&
    op.offset + op.text.length === prev.offset &&
    Path.equals(op.path, prev.path)
  ) {
    return true
  }

  return false
}

就是每次我们键入文本或者删除的时候,会通过这个合并策略,将该次操作op保存到同一个操作批次中,这样每次撤销和还原,就是这一整个批次(这点其实有些反操作习惯,比如我们连续打一大段文字的时候,每次撤销会把这一大段文字一并撤销掉。其实可以加一个类似防抖的操作,根据时间节点来优化这个合并策略。)

下面是撤销操作时的实现:

const batch = undos[undos.length - 1]
const inverseOps = batch.operations.map(Operation.inverse).reverse()
for (const op of inverseOps) {
  e.apply(op)
}
if (batch.selectionBefore) {
  Transforms.setSelection(e, batch.selectionBefore)
}

获取到上一次的编辑历史,然后从 Operation.inverse 中获取到该次编辑的反向操作:

// /slate/packages/slate/src/interfaces/operation.ts

inverse(op: Operation): Operation {
  switch (op.type) {
    case 'insert_node': {
      return { ...op, type: 'remove_node' }
    }

    case 'insert_text': {
      return { ...op, type: 'remove_text' }
    }

    case 'merge_node': {
      return { ...op, type: 'split_node', path: Path.previous(op.path) }
    }

    case 'move_node': {
      const { newPath, path } = op

      if (Path.equals(newPath, path)) {
        return op
      }

      if (Path.isSibling(path, newPath)) {
        return { ...op, path: newPath, newPath: path }
      }

      const inversePath = Path.transform(path, op)!
      const inverseNewPath = Path.transform(Path.next(path), op)!
      return { ...op, path: inversePath, newPath: inverseNewPath }
    }

    case 'remove_node': {
      return { ...op, type: 'insert_node' }
    }

    case 'remove_text': {
      return { ...op, type: 'insert_text' }
    }

    case 'set_node': {
      const { properties, newProperties } = op
      return { ...op, properties: newProperties, newProperties: properties }
    }

    case 'set_selection': {
      const { properties, newProperties } = op

      if (properties == null) {
        return {
          ...op,
          properties: newProperties as Range,
          newProperties: null,
        }
      } else if (newProperties == null) {
        return {
          ...op,
          properties: null,
          newProperties: properties as Range,
        }
      } else {
        return { ...op, properties: newProperties, newProperties: properties }
      }
    }

    case 'split_node': {
      return { ...op, type: 'merge_node', path: Path.next(op.path) }
    }
  }
},

如操作为 'insert_text' 则获取到的撤销操作是 'remove_text'。

并循环 reverse 数组,将获取到的 op 一个一个通过 apply 去执行。

在withHistory函数里,还新增了一个 writeHistory 方法。用于直接将操作批次(batch)写入历史记录。在每次 redoundo 操作时都会被调用。比如每次我运行一个撤销操作,那这次的历史操作就会被写入到还原栈中,反之每次运行还原操作时,也会被写入到撤销栈。