# 编写行为扩展

从一个插入问候语的命令开始，理解配置、storage、命令与生命周期。

假设你的产品需要一个“插入问候语”按钮。它只插入普通文字，不增加文档结构，因此使用 `Extension`。本章从已装有 StarterKit 的 Editor 开始；尚未创建编辑器时，先完成[快速开始](/guide/quick-start)。

## 1. 区分配置、运行状态和正文

| 数据 | 放在哪里 | 会随 JSON 保存吗 |
| --- | --- | --- |
| 按钮要插入的问候语 | `addOptions()`，由 `.configure()` 设置 | 不会 |
| 本次会话收到多少次内容更新 | `addStorage()` | 不会 |
| 插入后的文字 | 命令产生的正文 | 会 |

需要刷新后恢复的业务数据应进入节点或标记属性，不能只存在 storage。扩展对象会缓存自己的 storage；为每个 Editor 创建一个新扩展，避免两个编辑器共享可变状态。

## 2. 提供一个有类型的命令

`createGreeting()` 定义 `insertGreeting()`。命令内部使用现有 `insertContent()` 插入文本节点，所以 `<b>Hello</b>` 也是字面文字。它不把宿主提供的字符串当成 HTML。

### core/behavior/greeting.ts

```ts
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 });
}

```

### core/behavior/types.ts

```ts
/** Configuration belongs to the application, not to the document. */
export interface GreetingOptions {
  /** Literal text inserted by the greeting command. */
  text: string;
}

/** Runtime counters belong to one configured extension instance. */
export interface GreetingStorage {
  /** Accepted content update notifications, excluding dry runs. */
  updates: number;
}

```

文件中的 `declare module` 为 `editor.commands`、`chain()` 和 `can()` 补充同一个命令类型。声明只帮助 TypeScript；实际功能仍要安装扩展才存在。

## 3. 安装并调用

```ts [创建 Editor 时加入 extensions]
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 不会回滚之前已写入的步骤，详见[命令与事务](/concepts/commands)。

接下来学习[输入规则与状态插件](/guide/rules)，或为可持久内容添加[节点与布局](/build/extension)。完整钩子在[扩展 API](/api/extensions)查询。
