Skip to content

扩展开发指南

概述

diagram-js 的设计目标之一就是高度可扩展。通过模块化架构和依赖注入,你可以轻松地扩展现有功能或添加全新的功能。

本指南将介绍:

  • 如何创建自定义模块
  • 如何扩展现有服务
  • 如何实现自定义渲染
  • 实战案例与最佳实践

扩展方式

1. 创建自定义模块

自定义模块是扩展 diagram-js 的主要方式。

基本模块结构

typescript
// custom-module/CustomFeature.ts
export default class CustomFeature {
  static $inject = ["eventBus", "canvas"];

  constructor(
    private eventBus: EventBus,
    private canvas: Canvas,
  ) {
    this.init();
  }

  private init(): void {
    this.eventBus.on("element.click", (event) => {
      console.log("Element clicked:", event.element);
    });
  }

  public doSomething(): void {
    // 自定义功能实现
  }
}

模块定义

typescript
// custom-module/index.ts
import CustomFeature from "./CustomFeature";

export default {
  __depends__: [require("diagram-js/lib/core")],
  __init__: ["customFeature"],
  customFeature: ["type", CustomFeature],
};

使用自定义模块

typescript
import Diagram from "diagram-js";
import CustomModule from "./custom-module";

const diagram = new Diagram({
  canvas: { container: "#canvas" },
  modules: [CustomModule],
});

2. 扩展现有服务

通过继承或装饰器模式扩展现有服务。

扩展 Modeling 服务

typescript
import Modeling from "diagram-js/lib/features/modeling/Modeling";

export default class CustomModeling extends Modeling {
  static $inject = ["eventBus", "commandStack", "elementFactory"];

  // 添加新方法
  createCustomShape(attrs: any): Shape {
    return this.createShape({
      ...attrs,
      type: "custom:Shape",
    });
  }

  // 覆盖现有方法
  moveElements(elements: Element[], delta: Point, target?: Shape): void {
    // 添加自定义逻辑
    console.log("Moving elements with custom logic");

    // 调用父类方法
    super.moveElements(elements, delta, target);
  }
}

替换服务

typescript
// custom-module/index.ts
import CustomModeling from "./CustomModeling";

export default {
  __depends__: [require("diagram-js/lib/features/modeling")],
  __init__: ["modeling"],
  modeling: ["type", CustomModeling], // 替换默认的 Modeling
};

3. 自定义渲染器

自定义渲染器允许你完全控制元素的视觉表现。

创建渲染器

typescript
// custom-renderer/CustomRenderer.ts
import BaseRenderer from "diagram-js/lib/draw/BaseRenderer";
import {
  append as svgAppend,
  attr as svgAttr,
  create as svgCreate,
} from "tiny-svg";

const HIGH_PRIORITY = 1500;

export default class CustomRenderer extends BaseRenderer {
  static $inject = ["eventBus", "styles"];

  constructor(eventBus: EventBus, styles: any) {
    super(eventBus, HIGH_PRIORITY);
    this.styles = styles;
  }

  // 判断是否可以渲染该元素
  canRender(element: Element): boolean {
    return element.type && element.type.startsWith("custom:");
  }

  // 绘制形状
  drawShape(visuals: SVGElement, element: Shape): SVGElement {
    if (element.type === "custom:Circle") {
      return this.drawCircle(visuals, element);
    }

    if (element.type === "custom:Diamond") {
      return this.drawDiamond(visuals, element);
    }

    // 默认矩形
    return this.drawRect(visuals, element);
  }

  // 绘制连接线
  drawConnection(visuals: SVGElement, connection: Connection): SVGElement {
    const path = this.createPath(connection);

    const line = svgCreate("path");
    svgAttr(line, {
      d: path,
      stroke: "#000",
      "stroke-width": 2,
      fill: "none",
    });

    svgAppend(visuals, line);
    return line;
  }

  // 获取元素路径(用于连接线停靠)
  getShapePath(shape: Shape): string {
    if (shape.type === "custom:Circle") {
      return this.getCirclePath(shape);
    }

    return this.getRectPath(shape);
  }

  // === 私有辅助方法 ===

  private drawCircle(visuals: SVGElement, element: Shape): SVGElement {
    const cx = element.width / 2;
    const cy = element.height / 2;
    const r = Math.min(element.width, element.height) / 2;

    const circle = svgCreate("circle");
    svgAttr(circle, {
      cx,
      cy,
      r,
      fill: "#e3f2fd",
      stroke: "#1976d2",
      "stroke-width": 2,
    });

    svgAppend(visuals, circle);
    return circle;
  }

  private drawDiamond(visuals: SVGElement, element: Shape): SVGElement {
    const w = element.width;
    const h = element.height;

    const path = `M ${w / 2} 0 L ${w} ${h / 2} L ${w / 2} ${h} L 0 ${h / 2} Z`;

    const diamond = svgCreate("path");
    svgAttr(diamond, {
      d: path,
      fill: "#fff3e0",
      stroke: "#f57c00",
      "stroke-width": 2,
    });

    svgAppend(visuals, diamond);
    return diamond;
  }

  private drawRect(visuals: SVGElement, element: Shape): SVGElement {
    const rect = svgCreate("rect");
    svgAttr(rect, {
      x: 0,
      y: 0,
      width: element.width,
      height: element.height,
      rx: 5,
      fill: "#f5f5f5",
      stroke: "#333",
      "stroke-width": 2,
    });

    svgAppend(visuals, rect);
    return rect;
  }

  private createPath(connection: Connection): string {
    const waypoints = connection.waypoints;
    const path = waypoints
      .map((p, idx) => {
        return (idx === 0 ? "M" : "L") + p.x + "," + p.y;
      })
      .join(" ");

    return path;
  }

  private getCirclePath(shape: Shape): string {
    const cx = shape.x + shape.width / 2;
    const cy = shape.y + shape.height / 2;
    const r = Math.min(shape.width, shape.height) / 2;

    return `M ${cx - r} ${cy} 
            A ${r} ${r} 0 1 1 ${cx + r} ${cy}
            A ${r} ${r} 0 1 1 ${cx - r} ${cy} Z`;
  }

  private getRectPath(shape: Shape): string {
    const { x, y, width, height } = shape;
    return `M ${x} ${y} L ${x + width} ${y} L ${x + width} ${y + height} L ${x} ${y + height} Z`;
  }
}

注册渲染器

typescript
// custom-renderer/index.ts
import CustomRenderer from "./CustomRenderer";

export default {
  __depends__: [require("diagram-js/lib/core")],
  __init__: ["customRenderer"],
  customRenderer: ["type", CustomRenderer],
};

4. 实现自定义 CommandHandler

CommandHandler 用于实现可撤销的操作。

typescript
// custom-commands/UpdateColorHandler.ts
import {
  CommandHandler,
  CommandContext,
} from "diagram-js/lib/command/CommandHandler";

export default class UpdateColorHandler implements CommandHandler {
  execute(context: CommandContext): any {
    const { element, newColor } = context;

    // 保存旧颜色用于撤销
    context.oldColor = element.businessObject.color;

    // 更新颜色
    element.businessObject.color = newColor;

    return element;
  }

  revert(context: CommandContext): any {
    const { element, oldColor } = context;

    // 恢复旧颜色
    element.businessObject.color = oldColor;

    return element;
  }
}

注册 CommandHandler

typescript
// custom-commands/index.ts
import UpdateColorHandler from "./UpdateColorHandler";

export default {
  __depends__: [require("diagram-js/lib/command")],
  __init__: ["updateColorHandler"],
  updateColorHandler: ["type", UpdateColorHandler],
};

// 在服务中注册
class CommandRegistration {
  static $inject = ["commandStack"];

  constructor(commandStack: CommandStack) {
    commandStack.registerHandler("element.updateColor", UpdateColorHandler);
  }
}

最佳实践

1. 模块设计原则

单一职责

每个模块应该只负责一个功能领域。

typescript
// ✅ 好的设计:职责清晰
export default {
  __init__: ["customSelection"],
  customSelection: ["type", CustomSelection],
};

// ❌ 不好的设计:职责混杂
export default {
  __init__: ["everythingService"],
  everythingService: ["type", DoEverythingService],
};

依赖最小化

只依赖真正需要的服务。

typescript
// ✅ 好的设计
class CustomFeature {
  static $inject = ['eventBus'];  // 只需要 eventBus

  constructor(eventBus: EventBus) {
    this.eventBus = eventBus;
  }
}

// ❌ 不好的设计
class CustomFeature {
  static $inject = ['eventBus', 'canvas', 'elementRegistry', 'modeling'...];
  // 注入了很多不使用的服务
}

2. 事件命名规范

使用清晰的命名空间和动词时态。

typescript
// ✅ 好的命名
eventBus.on("custom.element.created", handler);
eventBus.on("custom.validation.failed", handler);

// ❌ 不好的命名
eventBus.on("myevent", handler);
eventBus.on("dostuff", handler);

3. TypeScript 类型定义

为自定义模块提供完整的类型定义。

typescript
// types.ts
export interface CustomElement extends Shape {
  customProperty: string;
  customData?: any;
}

export interface CustomModeling {
  createCustomElement(attrs: Partial<CustomElement>): CustomElement;
  updateCustomProperty(element: CustomElement, value: string): void;
}

// CustomModeling.ts
export default class CustomModeling implements CustomModeling {
  // 实现接口
}

4. 性能优化

事件防抖和节流

typescript
import { debounce, throttle } from "min-dash";

class CustomFeature {
  static $inject = ["eventBus"];

  constructor(eventBus: EventBus) {
    // 防抖:等待用户停止操作后执行
    eventBus.on(
      "element.changed",
      debounce((event) => {
        this.handleChange(event);
      }, 300),
    );

    // 节流:限制执行频率
    eventBus.on(
      "canvas.viewbox.changed",
      throttle((event) => {
        this.handleViewboxChange(event);
      }, 100),
    );
  }
}

批量操作

typescript
// ✅ 好的实现:批量执行
modeling.moveElements(selectedElements, delta);

// ❌ 不好的实现:逐个执行
selectedElements.forEach((element) => {
  modeling.moveElements([element], delta);
});

实战案例

案例 1:自定义工具面板(Palette)

创建一个自定义的工具面板,提供专用的元素创建工具。

typescript
// custom-palette/CustomPalette.ts
export default class CustomPalette {
  static $inject = ["palette", "create", "elementFactory", "handTool"];

  constructor(
    palette: Palette,
    create: Create,
    elementFactory: ElementFactory,
    handTool: HandTool,
  ) {
    palette.registerProvider(this);
  }

  getPaletteEntries(): PaletteEntries {
    const create = this.create;
    const elementFactory = this.elementFactory;

    function createShape(type: string) {
      return function (event: Event) {
        const shape = elementFactory.createShape({
          type: type,
          width: 100,
          height: 80,
        });
        create.start(event, shape);
      };
    }

    return {
      "custom-separator": {
        group: "custom",
        separator: true,
      },
      "create-custom-task": {
        group: "custom",
        className: "custom-icon-task",
        title: "创建任务",
        action: {
          dragstart: createShape("custom:Task"),
          click: createShape("custom:Task"),
        },
      },
      "create-custom-gateway": {
        group: "custom",
        className: "custom-icon-gateway",
        title: "创建网关",
        action: {
          dragstart: createShape("custom:Gateway"),
          click: createShape("custom:Gateway"),
        },
      },
      "hand-tool": {
        group: "tools",
        className: "bpmn-icon-hand-tool",
        title: "激活手型工具",
        action: {
          click: (event) => {
            this.handTool.activateHand(event);
          },
        },
      },
    };
  }
}

案例 2:自定义上下文菜单(ContextPad)

为特定类型的元素添加自定义的上下文操作。

typescript
// custom-context-pad/CustomContextPad.ts
export default class CustomContextPad {
  static $inject = ["contextPad", "modeling", "elementFactory", "connect"];

  constructor(
    contextPad: ContextPad,
    private modeling: Modeling,
    private elementFactory: ElementFactory,
    private connect: Connect,
  ) {
    contextPad.registerProvider(this);
  }

  getContextPadEntries(element: Element): ContextPadEntries {
    const modeling = this.modeling;
    const elementFactory = this.elementFactory;
    const connect = this.connect;

    // 只为自定义元素提供上下文菜单
    if (!element.type.startsWith("custom:")) {
      return {};
    }

    return {
      delete: {
        group: "edit",
        className: "context-pad-icon-remove",
        title: "删除元素",
        action: {
          click: function () {
            modeling.removeElements([element]);
          },
        },
      },
      connect: {
        group: "connect",
        className: "context-pad-icon-connect",
        title: "连接",
        action: {
          click: function (event) {
            connect.start(event, element);
          },
        },
      },
      "append-task": {
        group: "model",
        className: "context-pad-icon-task",
        title: "追加任务",
        action: {
          click: function (event) {
            const shape = elementFactory.createShape({
              type: "custom:Task",
              width: 100,
              height: 80,
            });

            modeling.appendShape(element, shape, {
              x: element.x + element.width + 80,
              y: element.y,
            });
          },
        },
      },
    };
  }
}

案例 3:自定义规则(Rules)

实现自定义的业务规则,控制哪些操作是允许的。

typescript
// custom-rules/CustomRules.ts
import RuleProvider from "diagram-js/lib/features/rules/RuleProvider";

export default class CustomRules extends RuleProvider {
  static $inject = ["eventBus"];

  constructor(eventBus: EventBus) {
    super(eventBus);
  }

  init(): void {
    // 连接规则
    this.addRule("connection.create", (context) => {
      const { source, target } = context;

      // 不允许连接到自己
      if (source === target) {
        return false;
      }

      // 自定义连接规则
      if (source.type === "custom:Start" && target.type === "custom:End") {
        return true;
      }

      return false;
    });

    // 移动规则
    this.addRule("elements.move", (context) => {
      const { shapes } = context;

      // 不允许移动开始节点
      return shapes.every((shape) => shape.type !== "custom:Start");
    });

    // 删除规则
    this.addRule("elements.delete", (context) => {
      const { elements } = context;

      // 不允许删除开始和结束节点
      return elements.every(
        (element) =>
          element.type !== "custom:Start" && element.type !== "custom:End",
      );
    });
  }
}

案例 4:领域特定编辑器

构建一个完整的领域特定图表编辑器。

typescript
// workflow-modeler/index.ts
import CoreModule from "diagram-js/lib/core";
import SelectionModule from "diagram-js/lib/features/selection";
import MoveModule from "diagram-js/lib/features/move";
import ModelingModule from "diagram-js/lib/features/modeling";
import ConnectModule from "diagram-js/lib/features/connect";
import CreateModule from "diagram-js/lib/features/create";
import ContextPadModule from "diagram-js/lib/features/context-pad";
import PaletteModule from "diagram-js/lib/features/palette";

import WorkflowRenderer from "./WorkflowRenderer";
import WorkflowPalette from "./WorkflowPalette";
import WorkflowContextPad from "./WorkflowContextPad";
import WorkflowRules from "./WorkflowRules";

export default {
  __depends__: [
    CoreModule,
    SelectionModule,
    MoveModule,
    ModelingModule,
    ConnectModule,
    CreateModule,
    ContextPadModule,
    PaletteModule,
  ],
  __init__: [
    "workflowRenderer",
    "workflowPalette",
    "workflowContextPad",
    "workflowRules",
  ],
  workflowRenderer: ["type", WorkflowRenderer],
  workflowPalette: ["type", WorkflowPalette],
  workflowContextPad: ["type", WorkflowContextPad],
  workflowRules: ["type", WorkflowRules],
};

使用编辑器:

typescript
import Diagram from "diagram-js";
import WorkflowModeler from "./workflow-modeler";

const modeler = new Diagram({
  canvas: { container: "#canvas" },
  modules: [WorkflowModeler],
});

// 导入数据
modeler.importDefinitions(workflowData);

调试技巧

1. 监听所有事件

typescript
eventBus.on("*", function (type, event) {
  console.log("Event:", type, event);
});

2. 检查服务注册

typescript
// 获取所有服务名称
const services = Object.keys(diagram._container._providers);
console.log("Registered services:", services);

// 检查服务是否存在
if (diagram.get("myService", false)) {
  console.log("myService is registered");
}

3. 性能分析

typescript
eventBus.on("commandStack.execute", function (event) {
  console.time(event.command);
});

eventBus.on("commandStack.executed", function (event) {
  console.timeEnd(event.command);
});

下一步

Released under the MIT License.