扩展命令

建议在阅读本小节内容之前先了解 Univer 的命令系统

创建新命令

创建新命令是 Univer 中最常见的扩展方式,通过创建新的命令,你可以实现各种业务逻辑。下面是创建新命令的步骤:

    创建一个实现 ICommand 接口的对象:

    TypeScript
    import type { IAccessor, ICommand } from '@univerjs/core'import { CommandType } from '@univerjs/core'export interface IYourCommandInterface {  // Your command's param's interface.}export const YourCommand: ICommand = {  name: 'your-command',  type: CommandType.COMMAND,  handler: async (accessor: IAccessor, params?: IYourCommandInterface) => {    // Implement your business logic here.  },}

    命令需要声明以下属性:

    1. name:命令的名称,必须唯一;我们建议以 业务域:类型:含义 的方式来命名,例如 sheet.command.copysheet.command.paste
    2. type:命令的类型
    3. handler:命令的执行逻辑,接收一个 IAccessor 对象以及命令参数,通过 IAccessor 对象可以访问 Univer 的依赖注入系统

    命令可接收参数,参数需要是一个对象,接口由你的业务逻辑决定。当然一个命令也可以不接收参数,此时 handler 的第二个参数为 undefined

    将 Command 注册到命令服务上:

    TypeScript
    import { Disposable, ICommandService } from '@univerjs/core'export class YourController extends Disposable {  constructor(@ICommandService private readonly _commandService: ICommandService) {    this.disposeWithMe(this._commandService.registerCommand(YourCommand))  }}

    之后就可以通过 ICommandService 来执行命令了。实践中常用的方式是通过 UI 触发。

Undo / Redo

Univer 提供了 undo redo 的能力,如果你的命令需要接入,需要在命令的 handler 回调函数中调用 IUndoRedoService 的对应方法:

TypeScript
import { IUndoRedoService } from '@univerjs/core'export const YourCommand: ICommand = {  name: 'your-command',  type: CommandType.COMMAND,  handler: async (accessor: IAccessor, params?: IYourCommandInterface) => {    const undoRedoService = accessor.get(IUndoRedoService)    undoRedoService.pushUndoRedo({      unitID: 'your-documents-id',      undoMutations: [/** mutations for undo */],      redoMutations: [/** mutations for do and redo */],    })  },}

扩展已有命令

除了创建新命令,Univer 还支持扩展已有的命令,这在扩展 Univer 内置能力时尤其必要。以下介绍三个比较典型的场景。

在其他 COMMAND 执行的时机补充 MUTATIONS

扩展复制粘贴

Univer 中的复制粘贴操作都是通过插件化的方式添加的,这意味着你可以:

  1. 修改复制/粘贴的默认处理过程;
  2. 在默认行为之外再追加一些逻辑。

你可以通过实现 ISheetClipboardHook 接口来构造一个你自己的 Hook 对象,并把它添加到 SheetClipboardService 上,Univer 默认的复制粘贴行为也是通过这一方式实现的。

Hook 对象包含多个可选的复制或粘贴钩子函数(Hook Function),你可以在后面的介绍了解所有钩子的定义和执行时机。

在复制粘贴的过程中,Univer 会调用 ISheetClipboardHook 的钩子函数,并按照特定规则及顺序执行。

创建和添加 Hook

TSX
import type { ISheetClipboardHook } from '@univerjs/sheets-ui'import { Disposable } from '@univerjs/core'import { ISheetClipboardService } from '@univerjs/sheets-ui'export class YourController extends Disposable {  constructor(@ISheetClipboardService private readonly _sheetClipboardService: ISheetClipboardService) {    super()    const yourHook: ISheetClipboardHook = {      id: 'your-hook-id',      onBeforeCopy: () => {        console.log('Hello!') // In this method, your code will be executed before copying.      },      // all hook methods are optional, you can learn it from interface definition.    }    // register your hook    this.disposeWithMe(this._sheetClipboardService.addClipboardHook(yourHook))  }}

使用 Hook 处理复制流程

复制和粘贴行为并不具有连贯性,复制的来源和粘贴去向不仅会在 Univer 内部,也可能在外部。因此,在 Hook 对象中,复制和粘贴可以进行独立的处理,你可以选择在 你的 Hook 中同时处理复制和粘贴,或只处理一个。Univer 复制粘贴的内容主要通过剪切板来存取,依赖clipboard.writeAPI。

在 Univer 中,可以通过快捷键和菜单触发复制操作。触发后,Univer 会将选区内容生成 HTML 和 PLAIN 写入剪切板。

复制和剪切共用 Hook Functions,它们只会在粘贴时进行区分。

Hook 中暴露如下方法来处理 HTML 的生成过程:

  1. onBeforeCopy: 这个 Hook Function 会在复制前执行,你可以在这里做一些前置工作。
  2. onCopyCellContent: 这个 Hook Function 会处理复制单元格的内容,它会决定生成的 HTML 中<table />中的<td />里的字符串内容。
  3. onCopyRow: 这个 Hook Function 会处理复制行属性,它会决定生成的 HTML 中<table />中的<tr />里的属性。
  4. onCopyColumn: 这个 Hook Function 会处理复制列属性,它会决定生成的 HTML 中<table />中的<colgroup />里的字符串内容。
  5. onAfterCopy: 这个 Hook Function 会在复制后执行。

使用 Hook 处理粘贴流程

在 Univer 中,可以通过快捷键和菜单触发粘贴操作。不同于复制,粘贴流程会触发 Mutation 来修改数据,因此关于粘贴的 Hook Function 基本都需要返回 Mutations 数组,需指定 Undo 和 Redo。在 Hook Function 的参数中,可以判断粘贴的来源是复制还是剪切。

Hook 中暴露如下方法来处理粘贴过程:

  1. onBeforePaste: 这个 Hook Function 会在粘贴前执行,你可以在这里做一些前置工作。

  2. onPasteCells: 这个 Hook Function 会处理粘贴单元格,它需要返回处理单元格内容的 Undo Mutations & Redo Mutations。

  3. onPasteRows: 这个 Hook Function 会处理粘贴行属性,它需要返回处理行属性的 Undo Mutations & Redo Mutations。

  4. onPasteColumns: 这个 Hook Function 会处理粘贴列属性,它需要返回处理列属性的 Undo Mutations & Redo Mutations。

  5. onAfterPaste: 这个 Hook Function 会在粘贴后执行。

例子:在 Univer 中复制粘贴数据格式

在 Univer 表格中,数字格式是一个上层模块,其信息独立于单元格信息之外。在只考虑内部复制粘贴的情况下,它需要在复制时主动去保存格式信息,并在粘贴时执行对应的添加数字格式的操作,因此只需实现 Hook 中的 onBeforeCopy 和 onPasteCells。在 onPasteCells 的实现中,需要区分是剪切还是复制。

TSX
import type { IRange } from '@univerjs/core'import { Disposable, Inject, Injector } from '@univerjs/core'import {  factoryRemoveNumfmtUndoMutation,  factorySetNumfmtUndoMutation,  IRemoveNumfmtMutationParams,  ISetNumfmtMutationParams,  RemoveNumfmtMutation,  SetNumfmtMutation,} from '@univerjs/sheets'import { COPY_TYPE, ISheetClipboardService } from '@univerjs/sheets-ui'export class NumfmtCopyPasteController extends Disposable {  constructor(    @Inject(Injector) private _injector: Injector,    @Inject(ISheetClipboardService) private _sheetClipboardService: ISheetClipboardService,  ) {    super()    this._initClipboardHook()  }  // register hook  private _initClipboardHook() {    this.disposeWithMe(      this._sheetClipboardService.addClipboardHook({        id: 'numfmt',        onBeforeCopy: (unitId, subUnitId, range) => this._collectNumfmt(unitId, subUnitId, range),        onPasteCells: (pastedRange, _m, _p, _copyInfo) =>          this._generateNumfmtMutations(pastedRange, { ..._copyInfo, pasteType: _p }),      }),    )  }  // collect number format info  private _collectNumfmt(unitId: string, subUnitId: string, range: IRange) {    // save number format info to private variable  }  // generate number format mutations  private _generateNumfmtMutations(    pastedRange: IRange,    copyInfo: {      copyType: COPY_TYPE      copyRange?: IRange      pasteType: string    },  ) {    if (copyInfo.copyType === COPY_TYPE.CUT) {      // remove number format info    }    if (copyInfo.pasteType === PASTE_TYPE.COPY) {      // add number format info      // const setRedos: ISetNumfmtMutationParams;      // const removeRedos: IRemoveNumfmtMutationParams;      return {        redos: [          { id: RemoveNumfmtMutation.id, params: removeRedos },          { id: SetNumfmtMutation.id, params: setRedos },        ],        undos: [          ...factorySetNumfmtUndoMutation(this._injector, setRedos),          ...factoryRemoveNumfmtUndoMutation(this._injector, removeRedos),        ],      }    }  }}

关于ISheetClipboardHook接口的详细定义如下

TypeScript
/** * `ClipboardHook` 可以: * 1. 在复制/剪切/粘贴之前,决定是否执行命令,并在必要时准备缓存。 * 2. 在复制时,决定哪些内容可以写入剪贴板。 * 3. 在粘贴时,获取剪贴板内容并将变更追加到粘贴命令。 */export interface ISheetClipboardHook {  /**   * 钩子的唯一 ID。   */  id: string  /**   * 是否为默认钩子。   */  isDefaultHook?: boolean  /**   * 只有特殊粘贴信息时,才会提供,该信息会替换默认钩子。   */  specialPasteInfo?: ISpecialPasteInfo  /**   * 钩子的优先级。优先级越高,钩子调用越早。   */  priority?: number  /**   * 在剪贴板服务决定需要复制的区域后调用的回调。   * 功能可以使用此钩子构建复制缓存或执行其他预复制任务。   * @param unitId   * @param subUnitId   * @param range   */  onBeforeCopy?: (unitId: string, subUnitId: string, range: IRange) => void  /**   * 需要附加到 td 元素的属性。   *   * @param row   * @param col   * @return content   */  onCopyCellContent?: (row: number, col: number) => string  /**   * 需要附加到 td 元素的属性。   *   * @deprecated 应该将其与 `onCopyCellContent` 合并到 `onCopyCell` 中   * @param row 复制的单元格的行   * @param col 复制的单元格的列   * @param rowSpan 复制的单元格的行跨度   * @param colSpan 复制的单元格的列跨度   * @return content   */  onCopyCellStyle?: (row: number, col: number, rowSpan?: number, colSpan?: number) => IClipboardPropertyItem | null  /**   * 需要附加到 tr 元素的属性。   * @param row 复制区域的每一行   * @return content   */  onCopyRow?: (row: number) => IClipboardPropertyItem | null  /**   * 需要附加到 col 元素的属性。   * @param col 复制区域的每一列   * @return content   */  onCopyColumn?: (col: number) => IClipboardPropertyItem | null  /**   * 在复制内容写入剪贴板后调用。   * 功能可以在这里做一些清理工作。   */  onAfterCopy?: () => void  // 不需要剪切钩子。我们可以使用复制钩子完成相同的操作,  // 并告诉粘贴钩子源自剪切命令。  // 与复制钩子不同,粘贴钩子**每次仅调用一次**,因为功能会执行批量变更。  /**   * 在剪贴板服务决定需要粘贴的区域后调用的回调。   * 功能可以使用此钩子构建粘贴缓存或执行其他预粘贴任务。   *   * @param pasteTo   * @returns 如果阻止复制,应该返回 false   */  onBeforePaste?: (pasteTo: ISheetDiscreteRangeLocation) => boolean  /**   * 处理粘贴单元格,需要返回处理单元格内容的撤销和重做变更。   *   * @param pasteFrom   * @param pasteTo   * @param data   * @param payload   * @returns 撤销和重做变更   */  onPasteCells?: (    pasteFrom: ISheetDiscreteRangeLocation | null,    pasteTo: ISheetDiscreteRangeLocation,    data: ObjectMatrix<ICellDataWithSpanInfo>,    payload: ICopyPastePayload,  ) => {    undos: IMutationInfo[]    redos: IMutationInfo[]  }  /**   * 处理粘贴的行属性,需要返回处理行属性的撤销和重做变更。   *   * @param pasteTo   * @param rowProperties   * @param payload   * @returns 撤销和重做变更   */  onPasteRows?: (    pasteTo: ISheetDiscreteRangeLocation,    rowProperties: IClipboardPropertyItem[],    payload: ICopyPastePayload,  ) => {    undos: IMutationInfo[]    redos: IMutationInfo[]  }  /**   * 处理粘贴的列属性,需要返回处理列属性的撤销和重做变更。   *   * @param pasteTo   * @param colProperties   * @param payload   * @returns 撤销和重做变更   */  onPasteColumns?: (    pasteTo: ISheetDiscreteRangeLocation,    colProperties: IClipboardPropertyItem[],    payload: ICopyPastePayload,  ) => {    undos: IMutationInfo[]    redos: IMutationInfo[]  }  /**   * 处理粘贴的纯文本,需要返回处理纯文本的撤销和重做变更。   *   * @param pasteTo   * @param text   * @param payload   * @returns 撤销和重做变更   */  onPastePlainText?: (    pasteTo: ISheetDiscreteRangeLocation,    text: string,    payload: ICopyPastePayload,  ) => {    undos: IMutationInfo[]    redos: IMutationInfo[]  }  /**   * 在粘贴内容写入 Univer 后调用。   * 功能可以在这里做一些清理工作。   *   * @param success 是否粘贴操作成功   */  onAfterPaste?: (success: boolean) => void  /**   * 在剪贴板服务决定需要复制或粘贴的区域之前调用的回调。   * 它会跳过这些被过滤的行,在复制或粘贴时忽略它们。   */  getFilteredOutRows?: (range: IRange) => number[]}

更多关于复制粘贴 Hook 的案例

扩展下拉填充

Univer 中的下拉填充也是通过插件化的方式添加的,类似复制粘贴,这意味着你可以通过实现接口ISheetAutoFillHook来添加一个 Hook 对象,从而修改和拓展下拉填充的行为。

创建和添加 Hook

TSX
import type { ISheetAutoFillHook } from '@univerjs/sheets-ui'import { Disposable } from '@univerjs/core'import { IAutoFillService } from '@univerjs/sheets-ui'export class YourController extends Disposable {  constructor(@IAutoFillService private readonly _autoFillService: IAutoFillService) {    super()    const yourHook: ISheetAutoFillHook = {      id: 'your-hook-id',      priority: 1,      type: AutoFillHookType.Append, // This hook will be executed after the default one      onBeforeFillData: (location, direction) => {        // In this method, you can cache the date in source range in case of refilling        console.log(`AutoFill will apply from Range-${location.source} to Range-${location.target}`)      },      onFillData: (location, direction, applyType) => {        console.log(`apply type is ${applyType}`)        // In this method, you can provide the mutations in redos which are supposed to be executed.        // Undos is also necessary.        return {          undos: [],          redos: [],        }      },      onAfterFillData: (location, direction, applyType) => {        console.log('AutoFill is completed.')      },      // all hook methods are optional, you can learn it from interface definition.    }    // register your hook    this.disposeWithMe(this._autoFillService.addHook(yourHook))  }}

修改默认下拉填充

除了选区变更以外,Univer 的默认下拉填充行为都是以 Hook 的形式添加的,在它的 Hook Function 中处理了扩充序列内容、样式等逻辑。

这个 Hook 与其他的 Hook 没什么不同,只是type: AutoFillHookType.Default而已,这类 Hook 只会有一个生效,并会第一个执行。因此,你完全可以自己写一个type: AutoFillHookType.Default的 Hook,只要它的 Priority 比默认的 0 大就行。

TypeScript
const yourHook: ISheetAutoFillHook = {  id: 'your-hook-id',  priority: 1,  type: AutoFillHookType.Default,  onBeforeFillData: (location, direction) => {    console.log(`AutoFill will apply from Range-${location.source} to Range-${location.target}`)  },  onFillData: (location, direction, applyType) => {    return {      undos: [],      redos: [],    }  },  onAfterFillData: (location, direction, applyType) => {    console.log('AutoFill is completed.')  },}

拓展下拉填充

如果在下拉填充时,希望执行一些额外的逻辑,比如让第三方的值也会跟随着下拉一起填充,你可以添加一个type: AutoFillHookType.Append的 Hook 对象,并在对应的 Hook Function 处理你的逻辑。这类 Hook 会在 AutoFillHookType.Default 后跟随执行,也可以通过 disable 方法去让它禁用。

TypeScript
const yourHook: ISheetAutoFillHook = {  id: 'your-hook-id',  priority: 0,  type: AutoFillHookType.Append,  disable: (location, direction, applyType) => true,  onBeforeFillData: (location, direction) => {    console.log(`AutoFill will apply from Range-${location.source} to Range-${location.target}`)  },  onFillData: (location, direction, applyType) => {    return {      undos: [],      redos: [],    }  },  onAfterFillData: (location, direction, applyType) => {    console.log('AutoFill is completed.')  },}

更多关于下拉填充 Hook 的案例

© 2026 DreamNum Co., Ltd.