---
title: 复合 Shape 的设计与实现
order: 3
---
G6 提供了灵活的 Shape 机制,支持开发者自定义各种图形,并在节点、边、Combo 等元素中高效复用。本文将以 Label(标签)为例,讲解如何自定义 Shape、如何在元素中应用。
## 一、Shape 的自定义与封装
### 1. Shape 的基类设计
所有 Shape 都继承自 `BaseShape`,它统一管理了 Shape 的生命周期(创建、更新、销毁)、属性解析、动画、事件绑定等。你只需关注如何实现 `render` 方法即可。
**核心抽象:**
```js
import { CustomElement } from '@antv/g';
abstract class BaseShape extends CustomElement {
// 生命周期管理、属性解析、动画等...
public abstract render(attributes, container): void;
}
```
### 2. 复合 Shape 层级结构示意
一个节点通常包含多个子 Shape,例如:
```
节点(Node)
├── keyShape(主图形)
├── label(标签,辅助信息)
│ ├── text(文本)
│ └── rect(背景)
├── icon(图标)
│ ├── text(文本)
│ └── image(图片)
├── badge(徽标)
│ ├── text(文本)
│ └── rect(背景)
└── port(锚点)
│ ├── circle(圆形)
```
### 3. Label Shape 的实现
Label 是一个典型的复合 Shape,由文本(Text)和可选的背景(Rect)组成。其实现思路如下:
- **属性分离**:Label 的样式属性分为文本样式和背景样式,分别传递给 Text 和 Rect。
- **智能布局**:背景自动根据文本内容、内边距、圆角等动态调整尺寸和位置。
- **复用 upsert**:通过 `upsert` 方法自动管理子 Shape 的创建、更新、销毁。
**Label 主要代码片段:**
```js
import { Text, Rect } from '@antv/g'; // 引入原子图形
export class Label extends BaseShape {
public render(attributes = this.parsedAttributes, container= this): void {
this.upsert('text', Text, this.getTextStyle(attributes), container);
this.upsert('background', Rect, this.getBackgroundStyle(attributes), container);
}
// ... 省略样式提取方法
}
```
- `getTextStyle`、`getBackgroundStyle` 分别提取文本和背景的样式属性,避免相互干扰。
- `upsert` 方法保证了 Shape 的自动增删改查,极大提升了复用性和健壮性。
### 4. 完整自定义 Shape 示例
下面以自定义一个带特殊装饰的标签为例,演示 Shape 的完整定义、注册与使用:
```js
import { BaseShape, ExtensionCategory, Circle } from 'g6';
import { Text, Rect, Circle } from '@antv/g';
class FancyLabel extends BaseShape {
render(attributes = this.parsedAttributes, container = this) {
// 主文本
this.upsert('text', Text, this.getTextStyle(attributes), container);
// 背景
this.upsert('background', Rect, this.getBackgroundStyle(attributes), container);
// 额外装饰:左侧小圆点
this.upsert('dot', Circle, {
x: -8, y: 0, r: 3, fill: '#faad14',
}, container);
}
// ...实现 getTextStyle/getBackgroundStyle
}
// 注册自定义 Shape
register(ExtensionCategory.SHAPE, 'fancy-label-shape', FancyLabel);
// 定义自定义节点
class CustomCircle extends Circle {
public drawFancyLabelShape(attributes, container) {
this.upsert('fancy-label', 'fancy-label-shape', this.getFancyLabelStyle(attributes), container);
}
render(attributes = this.parsedAttributes, container) {
super.render(attributes, container);
this.drawFancyLabelShape(attributes, container);
}
}
// 注册自定义节点
register(ExtensionCategory.Node, 'fancy-label-node', CustomCircle);
```
## 二、样式属性的前缀分离
G6 中节点、边、Combo 等元素往往包含多个子 Shape(如主图形、标签、徽标、锚点等)。为了让每个子 Shape 的样式互不干扰,G6 采用了**样式属性前缀分离**的设计。
### 1. 前缀分离的意义
- **解耦**:每个子 Shape 只关心属于自己的样式属性,避免样式污染。
- **易扩展**:新增子 Shape 只需定义新的前缀,无需修改原有逻辑。
- **配置直观**:用户在配置节点/边/Combo 时,可以一目了然地设置各部分样式。
### 2. 代码实现
以 Label 为例:
```ts
import { RectStyleProps, TextStyleProps } from '@antv/g';
type PrefixKey
= `${P}${Capitalize = {
[K in keyof T as K extends string ? PrefixKey : never]?: T[K];
};
interface LabelStyleProps extends TextStyleProps, Prefix<'background', RectStyleProps> {
background?: boolean;
}
```
- `Prefix<'background', RectStyleProps>` 表示所有以 `background` 开头的属性都属于标签背景样式。
- 在实际渲染时,通过 `subStyleProps`、`subObject` 等工具函数,自动提取带前缀的样式,传递给对应的 Shape。
**Label 背景样式提取示例**
```js
protected getBackgroundStyle(attributes: Required, container: Group): void {
const style = this.getLabelStyle(attributes);
this.upsert('label', Label, style, container);
}
public render(attributes = this.parsedAttributes, container: Group = this) {
// 1. 绘制 keyShape(主图形)
this._drawKeyShape(attributes, container);
if (!this.getShape('key')) return;
// 2. 绘制 halo
this.drawHaloShape(attributes, container);
// 3. 绘制 icon
this.drawIconShape(attributes, container);
// 4. 绘制 badges
this.drawBadgeShapes(attributes, container);
// 5. 绘制 label
this.drawLabelShape(attributes, container);
// 6. 绘制 ports
this.drawPortShapes(attributes, container);
}
```
- 每个子 Shape 的样式都通过前缀分离自动提取,传递给对应的 Shape 实例。
- 你可以通过配置项灵活控制每个子 Shape 的显示与样式。
### 2. 应用 Label 的示例
假设你要为节点添加带背景的标签,只需在节点数据中配置 label 相关属性:
```js
{
label: true,
labelText: '我是标签',
labelFill: '#333',
labelFontSize: 14,
labelBackground: true,
labelBackgroundFill: '#fffbe6',
labelBackgroundRadius: 6,
labelPadding: [4, 8],
}
```
- `labelText`、`labelFill`、`labelFontSize` 等会被自动提取并传递给 Label 的文本部分。
- `labelBackground`、`labelBackgroundFill`、`labelBackgroundRadius`、`labelPadding` 等会被自动提取并传递给 Label 的背景部分。
你无需手动管理 Label 的创建、更新、销毁,G6 会自动完成。
## 五、常见问题与调试建议
### 1. 为什么 label 样式没有生效?
- 检查样式属性前缀是否正确(如 `labelFill`、`labelBackgroundFill`)。
- 确认节点/边/Combo 的 `label` 配置为 `true`,且 `labelText` 已设置。
- 检查是否被其他样式覆盖。
### 2. 如何调试自定义 Shape 的渲染?
- 使用浏览器控制台查看 `shapeMap`,确认各子 Shape 是否被正确创建。
### 3. 如何让 Label 响应节点状态(如 hover、selected)?
- 直接在图配置中设置节点状态样式(推荐)
```js
const graph = new Graph({
node: {
style: {
label: false,
},
state: {
hover: {
label: true,
labelText: 'show when hovered',
},
},
},
});
```
- 或者在 Label 的实现中监听状态变化,动态调整样式。可以通过 data 获取到当前的状态值
---
如需更深入了解,建议阅读源码 [`base-shape.ts`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/shapes/base-shape.ts)、[`base-node.ts`](https://github.com/antvis/G6/blob/v5/packages/g6/src/elements/shapes/base-node.ts)。