代码语言

知识点思维导图

16 个知识节点

富文本编辑器(04) - 插件系统

读完后,你应能完成以下任务:

  • 绘制“富文本编辑器(04) - 插件系统 / 前言”的关键对象与数据流,解释“是一个基于 ProseMirror 的现代富文本编辑器,它提供了强大的插件系统,让开发者可以轻松扩展编辑器的功能。”,并用源码位置、日志或 Trace 标注证据。
  • 为“富文本编辑器(04) - 插件系统 / Nodes 和 Marks”设计正常与异常输入,验证“编辑器就是一棵树,Nodes 就是树上的节点,比如段落,代码块,Marks 就是给节点做装饰,改善用户体验,比如加粗,链接”,输出首个偏差位置与回归测试结果。
  • 实现“富文本编辑器(04) - 插件系统 / Node Mark Extension”的最小代码或配置,检验“Node 和 Mark 是跟页面元素相关的,Extension则是扩展功能”,输出命令、结果与 Diff,并说明不适用边界。

一、前言

TipTap 是一个基于 ProseMirror 的现代富文本编辑器,它提供了强大的插件系统,让开发者可以轻松扩展编辑器的功能。

二、Nodes 和 Marks

编辑器就是一棵树,Nodes 就是树上的节点,比如段落,代码块,Marks 就是给节点做装饰,改善用户体验,比如加粗,链接

三、Node Mark Extension

Node 和 Mark 是跟页面元素相关的,Extension则是扩展功能

四、插件系统架构

TipTap 的插件系统基于 ProseMirror 的插件架构,每个插件都可以:

  • 扩展编辑器的功能
  • 修改文档结构
  • 添加自定义命令
  • 处理用户交互
  • 管理编辑器状态

五、创建自定义插件

5.1 基础插件结构

下面是 bold 插件的源码

/** @jsxImportSource @tiptap/core */
import { Mark, markInputRule, markPasteRule, mergeAttributes } from '@tiptap/core'

export interface BoldOptions {
  /**
   * HTML attributes to add to the bold element.
   * @default {}
   * @example { class: 'foo' }
   */
  HTMLAttributes: Record<string, any>
}

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    bold: {
      /**
       * Set a bold mark
       */
      setBold: () => ReturnType
      /**
       * Toggle a bold mark
       */
      toggleBold: () => ReturnType
      /**
       * Unset a bold mark
       */
      unsetBold: () => ReturnType
    }
  }
}

/**
 * Matches bold text via `**` as input.
 */
export const starInputRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/

/**
 * Matches bold text via `**` while pasting.
 */
export const starPasteRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g

/**
 * Matches bold text via `__` as input.
 */
export const underscoreInputRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/

/**
 * Matches bold text via `__` while pasting.
 */
export const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g

/**
 * This extension allows you to mark text as bold.
 * @see https://tiptap.dev/api/marks/bold
 */
export const Bold = Mark.create<BoldOptions>({
  name: 'bold',

  addOptions() {
    return {
      HTMLAttributes: {},
    }
  },

  parseHTML() {
    return [
      {
        tag: 'strong',
      },
      {
        tag: 'b',
        getAttrs: node => (node as HTMLElement).style.fontWeight !== 'normal' && null,
      },
      {
        style: 'font-weight=400',
        clearMark: mark => mark.type.name === this.name,
      },
      {
        style: 'font-weight',
        getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null,
      },
    ]
  },

  renderHTML({ HTMLAttributes }) {
    return (
      <strong {...mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)}>
        <slot />
      </strong>
    )
  },

  markdownTokenName: 'strong',

  parseMarkdown: (token, helpers) => {
    // Convert 'strong' token to bold mark
    return helpers.applyMark('bold', helpers.parseInline(token.tokens || []))
  },

  renderMarkdown: (node, h) => {
    return `**${h.renderChildren(node)}**`
  },

  addCommands() {
    return {
      setBold:
        () =>
        ({ commands }) => {
          return commands.setMark(this.name)
        },
      toggleBold:
        () =>
        ({ commands }) => {
          return commands.toggleMark(this.name)
        },
      unsetBold:
        () =>
        ({ commands }) => {
          return commands.unsetMark(this.name)
        },
    }
  },

  addKeyboardShortcuts() {
    return {
      'Mod-b': () => this.editor.commands.toggleBold(),
      'Mod-B': () => this.editor.commands.toggleBold(),
    }
  },

  addInputRules() {
    return [
      markInputRule({
        find: starInputRegex,
        type: this.type,
      }),
      markInputRule({
        find: underscoreInputRegex,
        type: this.type,
      }),
    ]
  },

  addPasteRules() {
    return [
      markPasteRule({
        find: starPasteRegex,
        type: this.type,
      }),
      markPasteRule({
        find: underscorePasteRegex,
        type: this.type,
      }),
    ]
  },
})

六、编辑器常用字段

6.1 扩展配置

addOptions

用于配置选项,也就是 extension.configure({})

import { Extension } from "@tiptap/core"

const MyExtension = Extension.create({
  name: "myExtension",
  addOptions: {
    myOption: "myOption"
  }
})

export default MyExtension

group

表示分组到块级元素

import { Extension } from "@tiptap/core"

const MyExtension = Extension.create({
  name: "myExtension",
  group: "block" // 告诉编辑器:我是一个块级元素
})

content

content: 'block+' 表示至少有一个块级元素,块级元素如 'paragraph | heading | codeBlock | blockquote | list'

import { Extension } from "@tiptap/core"

const MyExtension = Extension.create({
  name: "myExtension",
  content: "block+"
})

defining

默认是 false,设置为 true 时,光标在代码块内部时,上下箭头键不会轻易跳出代码块,需要明确的操作(如 Enter、Escape)才能离开。

import { Extension } from "@tiptap/core"

const MyExtension = Extension.create({
  name: "myExtension",
  defining: true
})

parseHTML

用于解析 HTML 为 ProseMirror 节点

import { Extension } from "@tiptap/core"

const MyExtension = Extension.create({
  name: "myExtension",
  parseHTML() {
    return [
      {
        tag: "span",
        getAttrs: (node) => {
          return {
            class: node.getAttribute("class")
          }
        }
      }
    ]
  }
})

renderHTML

用于渲染 HTML 为 ProseMirror 节点

const CustomMark = Mark.create({
  name: "customMark",

  renderHTML({ HTMLAttributes }) {
    return ["span", HTMLAttributes, 0]
  }
})

addCommands

用于定义扩展命令,用户可以执行的命令

declare module "@tiptap/core" {
  interface Commands<ReturnType> {
    customExtension: {
      customCommand: () => ReturnType
    }
  }
}

const CustomExtension = Extension.create({
  name: "customExtension",

  addCommands() {
    return {
      customCommand:
        () =>
        ({ commands }) => {
          return commands.setContent("Custom command executed")
        }
    }
  }
})

使用

editor.commands.customCommand() // 'Custom command executed'
editor.chain().customCommand().run() // 'Custom command executed'

addAttributes

用于定义自定义属性

const CustomMark = Mark.create({
  name: "customMark",

  addAttributes() {
    return {
      customAttribute: {
        default: "value",
        parseHTML: (element) => element.getAttribute("data-custom-attribute")
      }
    }
  }
})

addKeyboardShortcuts

用于定义扩展键盘快捷键

const CustomExtension = Extension.create({
  name: "customExtension",

  addKeyboardShortcuts() {
    return {
      "Mod-k": () => {
        console.log("Keyboard shortcut executed")
      }
    }
  }
})

6.2 添加插件到编辑器

import { Editor } from "@tiptap/core"
import { StarterKit } from "@tiptap/starter-kit"
import { customPlugin } from "./customPlugin"

const editor = new Editor({
  extensions: [StarterKit, customPlugin()]
})

七、常用插件类型

7.1 命令插件

Mark.create 和 Node.create 表示不同节点插件,而 Extension.create 表示功能插件可以修改编辑器行为,没有新的节点

import { Extension } from "@tiptap/core"

export const CustomCommand = Extension.create({
  name: "customCommand",

  addCommands() {
    return {
      insertCustomContent:
        () =>
        ({ commands }) => {
          return commands.insertContent("<p>自定义内容</p>")
        }
    }
  }
})

7.2 节点插件

import { Node } from "@tiptap/core"

export const CustomNode = Node.create({
  name: "customNode",

  group: "block",
  content: "inline*",

  parseHTML() {
    return [{ tag: "div[data-custom]" }]
  },

  renderHTML({ HTMLAttributes }) {
    return ["div", { ...HTMLAttributes, "data-custom": "" }, 0]
  }
})

7.3 标记插件

import { Mark } from "@tiptap/core"

export const CustomMark = Mark.create({
  name: "customMark",

  parseHTML() {
    return [{ tag: "span[data-custom]" }]
  },

  renderHTML({ HTMLAttributes }) {
    return ["span", { ...HTMLAttributes, "data-custom": "" }, 0]
  }
})

八、高级插件功能

8.1 状态管理

import { Extension } from "@tiptap/core"

export const StatePlugin = Extension.create({
  name: "statePlugin",

  addStorage() {
    return {
      count: 0
    }
  },

  addCommands() {
    return {
      incrementCount:
        () =>
        ({ editor }) => {
          const currentCount = editor.storage.statePlugin.count
          editor.storage.statePlugin.count = currentCount + 1
          return true
        }
    }
  }
})

8.2 事件处理

import { Extension } from "@tiptap/core"

export const EventPlugin = Extension.create({
  name: "eventPlugin",

  onCreate() {
    console.log("编辑器创建")
  },

  onUpdate() {
    console.log("内容更新")
  },

  onSelectionUpdate() {
    console.log("选择更新")
  },

  onDestroy() {
    console.log("编辑器销毁")
  }
})

九、插件最佳实践

9.1 性能优化

  • 避免在插件中进行昂贵的计算
  • 使用防抖处理频繁的事件
  • 合理使用状态缓存

9.2 错误处理

export const SafePlugin = Extension.create({
  name: "safePlugin",

  addCommands() {
    return {
      safeCommand:
        () =>
        ({ editor }) => {
          try {
            // 执行命令
            return true
          } catch (error) {
            console.error("命令执行失败:", error)
            return false
          }
        }
    }
  }
})

9.3 配置选项

export const ConfigurablePlugin = Extension.create({
  name: "configurablePlugin",

  addOptions() {
    return {
      enabled: true,
      customOption: "default"
    }
  },

  onCreate() {
    if (!this.options.enabled) {
      return
    }
    // 插件逻辑
  }
})

9.4 editor 上的方法

插件开发的基础就是灵活使用插件的方法还有 editor 上的方法

  • editor.getAttributes('textStyle') 用于获取当前选中文本或光标位置的属性信息。
  • lift 解除

十、总结

  • 前言:是一个基于 ProseMirror 的现代富文本编辑器,它提供了强大的插件系统,让开发者可以轻松扩展编辑器的功能。
  • Nodes 和 Marks:编辑器就是一棵树,Nodes 就是树上的节点,比如段落,代码块,Marks 就是给节点做装饰,改善用户体验,比如加粗,链接
  • Node Mark Extension:Node 和 Mark 是跟页面元素相关的,Extension则是扩展功能
  • 插件系统架构:TipTap 的插件系统基于 ProseMirror 的插件架构,每个插件都可以:
  • 编辑器常用字段:用于配置选项,也就是 extension.configure({})
  • 常用插件类型:Mark.create 和 Node.create 表示不同节点插件,而 Extension.create 表示功能插件可以修改编辑器行为,没有新的节点

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“插件系统”中,需要同时满足“前言”与“Nodes 和 Marks”。给定正文约束“是一个基于 ProseMirror 的现代富文本编辑器,它提供了强大的插件系统,让开发者可以轻松扩展编辑器的功能。”,哪些判断保持了原有处理机制?多选
2“插件系统”出现偏差:“在“插件系统 / Node Mark Extension”中,即使不满足“Node 和 Mark 是跟页面元素相关的,Extension则是扩展功能”,结果与副作用仍会保持不变。”已成为实际行为。围绕“Node Mark Extension”与“插件系统架构”,哪些判断能定位被改变的职责或边界?多选
3评审“插件系统”方案时,验收条件包含“用于配置选项,也就是 extension.configure({})”。关于“addOptions”与“content”的哪些决策符合正文机制?多选