假设你的产品需要一个“插入问候语”按钮。它只插入普通文字,不增加文档结构,因此使用 Extension。本章从已装有 StarterKit 的 Editor 开始;尚未创建编辑器时,先完成快速开始。
1. 区分配置、运行状态和正文
| 数据 | 放在哪里 | 会随 JSON 保存吗 |
|---|---|---|
| 按钮要插入的问候语 | addOptions(),由 .configure() 设置 | 不会 |
| 本次会话收到多少次内容更新 | addStorage() | 不会 |
| 插入后的文字 | 命令产生的正文 | 会 |
需要刷新后恢复的业务数据应进入节点或标记属性,不能只存在 storage。扩展对象会缓存自己的 storage;为每个 Editor 创建一个新扩展,避免两个编辑器共享可变状态。
2. 提供一个有类型的命令
createGreeting() 定义 insertGreeting()。命令内部使用现有 insertContent() 插入文本节点,所以 <b>Hello</b> 也是字面文字。它不把宿主提供的字符串当成 HTML。
import { Extension } from "@quirekit/core";
import type { GreetingOptions, GreetingStorage } from "./types";
declare module "@quirekit/core" {
/** Application commands installed by the greeting extension. */
interface Commands<ReturnType> {
/** Greeting behavior. */
greeting: {
/** @returns Whether insertion can run or was accepted. */
insertGreeting: () => ReturnType;
};
}
}
/**
* Creates fresh options/storage for each editor instead of sharing a mutable extension.
* @param text - Literal greeting, never interpreted as HTML.
* @returns One installable, configured behavior.
*/
export function createGreeting(text = "Hello!") {
return Extension.create<GreetingOptions, GreetingStorage>({
name: "greeting",
/** @returns Application defaults. */
addOptions() { return { text: "Hello!" }; },
/** @returns Session-local state; it is not document JSON. */
addStorage() { return { updates: 0 }; },
/** @returns The same command for direct, chain and can modes. */
addCommands() {
return {
insertGreeting: () => ({ commands }) => this.options.text.length > 0
&& commands.insertContent({ type: "text", text: this.options.text }),
};
},
/** Counts accepted document updates; querying can() does not run this hook. */
onUpdate() { this.storage.updates += 1; },
}).configure({ text });
}
文件中的 declare module 为 editor.commands、chain() 和 can() 补充同一个命令类型。声明只帮助 TypeScript;实际功能仍要安装扩展才存在。
3. 安装并调用
extensions: [StarterKit, createGreeting('你好!')]
按钮点击时调用 editor.chain().focus().insertGreeting().run();按钮可用性读取 editor.can().insertGreeting()。输入焦点不是权限检查,应用仍需确认当前用户可以修改文档。
命令必须支持 can():不发请求、不改 storage、不操作 DOM。例程把插入交给现有命令,内容真正更新后才由 onUpdate 增加计数。查询按钮可用性不会增加它。
4. 配置与继承分别做什么
.configure({ text: '欢迎' }) 调整已有公开选项;.extend({ ... }) 修改扩展定义,例如覆盖快捷键。覆盖已有钩子又需要原行为时,通过该钩子的 this.parent?.() 调用父实现。
只安装最终扩展。把原扩展与同名的 .extend() 结果一起装入,会造成名称、Schema 或命令冲突。priority 可以调整扩展处理顺序,数值越大越早;不要用优先级掩盖两个模块争用同一功能的问题。
5. 如何验证
调用 can() 后正文与 storage 应不变;执行命令后应出现问候语,撤销后消失。创建两个 Editor,分别传入两个 createGreeting() 结果,一个编辑器的计数不应影响另一个。计数也不会出现在下载的 JSON 中。
复杂命令应通过传入的 tr、commands 与 chain 组合,避免在命令内另起 editor.commands 事务。链中后续步骤返回 false 不会回滚之前已写入的步骤,详见命令与事务。