antvis/G6

[V5]The custom node will call drawKeyShape when the construction of the custom node is not completely completed, resulting in an exception #6232

zmbxy posted onGitHub

Describe the bug / 问题描述

自定义节点时,通过debug打印执行顺序发现,自定义节点时,在constructor中完成super调用后,在执行constructor中super之后代码之前,执行drawKeyShape,此时,由于自定义节点未完成构造,导致在drawKeyShape中提前访问了未配置好的数据,出现异常错误。

<img width="1192" alt="image" src="https://github.com/user-attachments/assets/c024536c-8d24-4cf0-9b6d-c9b763ca8de6">

https://stackblitz.com/edit/vitejs-vite-wfzclz?file=src%2FApp.tsx

Steps to Reproduce the Bug or Issue / 重现步骤

G6 Version / G6 版本

🆕 5.x

Operating System / 操作系统

macOS

Browser / 浏览器

Chrome

Additional context / 补充说明

No response


你可以更详细描述你的问题

posted by Aarebecca 8 months ago
constructor(options: RouterNodeStyleProps) {
    console.log('1. svg node constructor...');
    super(options);
    console.log('2. svg node constructor...');

    // 自定义节点的配置,实际是通过options传入的
    this.getType = (data: NodeData) => SvgNodeType;
  }

第我期望写一个自定义节点,继承自BaseNode它能够根据业务数据,绘制不同的svg图标。如constructor,我提供了一个getType方法用来根据业务数据获取节点类型

protected drawKeyShape(attributes: Required<RouterNodeStyleProps>, shapeGroup: Group) {
    const graph = attributes.context.graph;
    const nodeId = shapeGroup.id;
    const nodeData = graph.getNodeData(nodeId);

    // 这里提前访问了constructor中未完成定义的变量,报错
    const svgNodeType = this.getType(nodeData);
    const svgPath = this.nodeMap[svgNodeType];

    return undefined;
}

然后在绘制图形(调用drawKeyShape)时,调用this.getType,并传入当前节点的原始业务数据,绘制对应的节点图形。

但现在的状况是,由于自定义节点中的函数调用,会在调用super方法后,执行this.getType赋值之前,调用drawKeyShape函数,此时会在drawKeyShape中调用一个未定义的this.getType属性报错。

posted by zmbxy 8 months ago

@zmbxy 为什么要在构造函数中赋值 this.getType,直接定义为成员方法不就行了吗

posted by Aarebecca 8 months ago

我现在有两张不同业务的图,它们的节点图形不同,数据不同,我希望我的自定义节点能够在外部动态配置svg节点或者后续的替换也好,把svg节点绘制这块统一一下,外部自行根据节点数据,绘制不同的图形,如果不能动态配置,那我只能在drawKeyShape中通过写代码逻辑控制,但这样一来,实现就很麻烦了,需要写很多硬编码在里面了。

posted by zmbxy 8 months ago

@Aarebecca 我参考G6实现,写了一段简短的代码来复现这个问题,和我前面提到的那个问题是相同的结果

interface BaseShapeOptions {
  a: number;
  b: string;
}

abstract class BaseShape {
  options: BaseShapeOptions;

  constructor(options: BaseShapeOptions) {
    this.options = options;
    this.render();
    console.log('this is is CustomNode: ', this instanceof CustomNode);
  }

  public abstract render(): void;
}

abstract class BaseNode extends BaseShape {

  constructor(options: BaseShapeOptions) {
    super(options);
  }

  public abstract drawKeyShape(): void;

  render(): void {
    this.drawKeyShape();
  }
}

class CustomNode extends BaseNode {

  c: number;

  constructor(options: BaseShapeOptions) {
    console.log('========== 1.开始实例化自定义节点 ==========');
    super(options);
    console.log('========== 2.初始化自定义节点独有的属性 ==========');
    this.c = 10;
    console.log('========== 3.自定义节点实例化完成 ==========');
  }

  drawKeyShape(): void {
    console.log('========== 4.调用绘制图形方法,绘制图形 ==========')
  }
}

new CustomNode({ a: 1, b: '2' });

以上代码的打印结果如下:

========== 1.开始实例化自定义节点 ==========
========== 4.调用绘制图形方法,绘制图形 ==========
base shape this:  true
========== 2.初始化自定义节点独有的属性 ==========
========== 3.自定义节点实例化完成 ==========

出现问题的点在于abstract class BaseShape,在实例化时就调用了render函数导致的,使用面向对象编程,我们是为了易扩展、易维护,而现在这种状况,除了能重写一下drawKeyShape,其它的好像也扩展不了什么,就丢失了它的意义了。

而且从另一个角度来说,我的自定义节点未实例化完成,就去调用它的方法,这种感觉就像是我定义了一个关于汽车的要点要素,然后我去按照这个框架,自己造一辆车,但是我车没造好,就要它能跑起来,这显然是不合理的。

这只是我自己的一些想法,建议团队可以考虑一下。

posted by zmbxy 8 months ago

@Aarebecca 哈喽,这个问题我看好几天没有回复了,这个还有在跟踪吗?

我们目前还在用4.x版本,现在也在了解,熟悉v5版本新API,为v4版本迁移做准备,如果团队不认为这是个问题或者设计就是如此,那我就想其它方法来封装了。

posted by zmbxy 8 months ago

@zmbxy G6 5.0 的元素基类会在构造方法中调用 render 以执行各部分图形的绘制流程。当要进行元素自定义时,通常有以下两种途径:

  1. 直接覆写基类的绘制方法,例如覆写 drawKeyShape 将主图形绘制为其他图形

    可以参考内置节点的实现,该情况下需要遵从基类元素的绘制流程

    class Circle extends BaseNode {
    drawKeyShape(attributes, container) {
     // draw key shape here
    }
    }
  2. 新增自定义绘制方法,例如新增 drawCustomShape 方法,并在 render 中调用该方法

    使用该方式你可以在不影响 render逻辑的情况下任意定制你的绘制流程

    // 在圆形节点的基础上额外绘制自定义元素
    class CustomNode extends Circle {
    render(attributes, container) {
     super.render(attributes, container);
     this.drawCustomShape(attributes, container);
    }
    
    drawCustomShape(attributes, container) {
     // draw custom shape here
    }
    }

"abstract class BaseShape,在实例化时就调用了render函数" 至于你提到的问题,如果你想充分的定制绘制逻辑,可以覆写 render 方法,并不要调用 super.render,然后完全重新编写绘制逻辑即可

posted by Aarebecca 8 months ago

@Aarebecca <img width="1228" alt="image" src="https://github.com/user-attachments/assets/bb603cc4-9e36-43b6-850b-5f1bb825f693"> 那请问一下,如果要自定义属性,有办法么?

posted by zmbxy 8 months ago

@zmbxy

自定义属性的传入: Graph Options

const graph = new Graph({
  node: {
    style: {
      customStyle: d => d.data.xxx // 使用数据属性作为自定义属性
    },
  },
});

消费自定义属性:

class CustomNode extends Circle {
  render(attributes, container) {
    super.render(attributes, container);
    this.drawCustomShape(attributes, container);
  }

  drawCustomShape(attributes, container) {
    // get custom style from attributes
    const { customStyle } = attributes;
  }
}
posted by Aarebecca 8 months ago

好的🙏🙏🙏🙏

posted by zmbxy 8 months ago

Fund this Issue

$0.00
Funded

Pull requests