代码语言
知识点思维导图
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 表示功能插件可以修改编辑器行为,没有新的节点
学完自测
选择所有正确答案;提交后逐项核对判断依据。