简介:这款拓扑图插件基于antvg6封装,面向网络管理员、系统架构师及前端开发者,用于构建可自定义的网络拓扑可视化方案。包内共29个文件,以14个js核心代码、7个gif动态演示、2个json配置和2个png图标为主,另含说明txt、docx附赠资源及README,整体仅1.2MB,结构清晰便于快速集成。插件内置缩放节点、警告节点、播放警告节点等多样节点,并提供流动边、信息流边、警告流边、二次标签边等动态连线,可直观呈现数据流向与告警状态,同时支持自定义右键菜单,满足个性化交互需求。该插件通过颜色、样式和动画变化强化异常提示,基于成熟antvg6框架保证了良好的兼容性与可扩展性,能轻松融入现有前端项目。已有80人学习下载,附带的示例代码、效果图及配置文档可帮助快速上手,适用于深度分析和管理复杂网络架构。
1. 为什么用 AntV G6 而不是从零画拓扑图
做网络拓扑图插件时,最常被问到的就是:AntV G6 能不能支持自定义节点和动态连线?答案是可以,但直接写底层配置很繁琐。这套基于 AntV G6 封装的拓扑图插件,把最常用的几种节点和连线预先实现好,开箱即用。它内置缩放节点、警告节点、播放警告节点,以及流动边、信息流边、警告流边、二次标签边,同时把右键菜单行为暴露成可配置项。对于网络管理员和系统架构师,它能把设备状态、链路质量直接映射成视觉变化;对于前端工程师,它是一个可以二次开发的 G6 扩展包。我更关注的是它的实现方式:什么时候用自定义节点,什么时候用 setState,动画怎么不加戏,菜单怎么不打断操作。这些在后面的章节都会拆开讲。
2. 自定义节点:缩放、警告、播放警告的实现与选型
2.1 G6 自定义节点的注册机制
G6 中所有自定义节点都走G6.registerNode(name, methods, parentType)。第一个参数是节点类型名,数据里type字段直接引用;第二个参数是重写的方法集合;第三个参数指定继承的内置类型,常见是'circle'、'rect'、'ellipse'。这个插件里默认继承'circle',因为网络拓扑里的主机、路由器、交换机大多用圆形标识,圆形也更容易做缩放和告警光晕。
下面的代码演示了如何在draw方法里拿到配置并返回图形组。cfg是当前节点数据,group是 G6 的图形容器,可以往里添加多个图形元素。
G6.registerNode('warningNode', { draw(cfg, group) { const { size = 40, warning = false } = cfg; const keyShape = group.addShape('circle', { attrs: { x: 0, y: 0, r: size / 2, fill: warning ? '#fff1f0' : '#e6fffb', stroke: warning ? '#ff4d4f' : '#13c2c2', lineWidth: warning ? 3 : 1.5 }, name: 'keyShape' }); if (warning) { group.addShape('text', { attrs: { x: 0, y: 0, textAlign: 'center', textBaseline: 'middle', text: '!', fill: '#ff4d4f', fontSize: 16, fontWeight: 'bold' }, name: 'warning-text' }); } return keyShape; } }, 'circle');这里的要点是draw必须返回keyShape,G6 后续的setState、碰撞检测都以它为主。name字段用来在group.find(e => e.get('name') === 'xxx')里定位图形,推荐给所有子图形都起名,否则后续动画找不到目标。warningNode这个名字对应资源包里的warningNode.png,数据里只要把type写成'warningNode'就能直接使用。
2.2 缩放节点:把业务指标映射到 node size
缩放节点在插件里叫zoomNode,它的核心不是绘制新形状,而是把数据里的数值字段映射成节点半径。我会在注册前做一次归一化,避免直接拿 CPU 使用率这种原始值画圆。下面这个例子读取value字段,范围 0 到 100,映射到 16 到 80 像素的半径。
G6.registerNode('zoomNode', { draw(cfg, group) { const { value = 50, minSize = 16, maxSize = 80 } = cfg; const ratio = Math.max(0, Math.min(1, value / 100)); const r = minSize + (maxSize - minSize) * ratio; return group.addShape('circle', { attrs: { x: 0, y: 0, r, fill: '#1890ff', fillOpacity: 0.2, stroke: '#1890ff', lineWidth: 2 }, name: 'keyShape' }); } }, 'circle');实际工程里不建议把value / 100写死。我在封装时会从cfg.maxValue、cfg.minValue读取真实刻度再归一化,这样负载 50% 和流量 500MB/s 都能映射到同一套比例。缩放节点适合表达流量、负载、在线人数这类连续指标。如果指标超过阈值,可以额外叠加一个红色外环,这个外环放在draw里画第二个circle即可。注意外环不要带name: 'keyShape',否则setState会错误更新到它。
下面是一张节点选型对照表,方便在写业务时快速决定用哪种。
| 节点类型 | 视觉特征 | 适用数据字段 | 动画复杂度 |
|---|---|---|---|
| zoomNode | 半径随数值变化 | 负载、流量、在线人数 | 低 |
| warningNode | 红蓝切换 + 三角感叹号 | 告警状态、健康度 | 低 |
| playWarnNode | 光晕或闪烁扩散 | 正在播放的警告事件 | 中 |
2.3 警告节点与播放警告节点:状态与动画叠加
警告节点的warning字段控制颜色,实际使用中警告状态会由后端推送或者轮询得到,因此需要在update或setState中响应变化。一个常见做法是给节点注册自定义状态,而不在draw里写死颜色。播放警告节点在插件里叫playWarnNode,它在警告节点的外观之上加了一层扩散波纹动画,提醒操作者这是「正在发生的告警」而不是「历史告警」。
G6.registerNode('playWarnNode', { draw(cfg, group) { const keyShape = group.addShape('circle', { attrs: { x: 0, y: 0, r: 20, fill: '#fff1f0', stroke: '#ff4d4f', lineWidth: 3 }, name: 'keyShape' }); group.addShape('circle', { attrs: { x: 0, y: 0, r: 10, fill: 'rgba(255, 77, 79, 0.4)', stroke: '#ff4d4f', lineWidth: 2 }, name: 'warn-ring' }); return keyShape; }, afterDraw(cfg, group) { const ring = group.find(e => e.get('name') === 'warn-ring'); if (!ring) return; ring.animate((ratio) => { return { r: 10 + ratio * 30, opacity: 1 - ratio }; }, { duration: 1500, repeat: true }); } }, 'circle');插件包里的animate目录正是把这些动画单独拆分,便于在图表空闲时批量停掉。播放警告节点和警告节点最大的差别是:警告节点是静态状态,播放警告节点是事件流。前者用setState('warning', true)触发,后者通常在新增数据时插入一个带playWarnNode的节点,播放完再删除或转为普通警告节点。如果在afterDraw里直接启动动画,要小心重复执行,G6 在视图更新时会多次调用afterDraw,所以最好用节点身上的__playFlag字段做防重入。
提示:如果发现动画导致页面频繁重绘,可以先调用
graph.stopAnimate(true),在需要时再手动启动。大数据量拓扑图建议默认关动画,只对高亮节点开。
3. 动态连线:流动边、信息流边、警告流边与二次标签边
3.1 边的数据模型与注册入口
G6 的边和节点类似,用G6.registerEdge(name, methods, parentType)注册。边数据中至少有source和target,type指定边类型。这个插件里的四种边分别解决不同的问题:flowEdge负责传达正在发生的数据流,flowInfoEdge在流动基础上追加文字说明,flowWarnEdge用颜色和脉冲表达异常,quadraticLabelEdge解决复杂拓扑中的标签重叠。选型时先问一个问题:这条边除了连接两个节点,还要让读者看到什么?
3.2 流动边与信息流边:动画路径与流向标识
流动边的做法是给lineDash配一个动画。G6 里可以覆写afterDraw,用shape.animate更新 dash 偏移量。因为 G6 的边默认会生成名为keyShape的图形,所以直接找到它再启动动画,比遍历group.get('children')更可靠。
G6.registerEdge('flowEdge', { afterDraw(cfg, group) { const shape = group.find(e => e.get('name') === 'keyShape'); if (!shape) return; const length = shape.getTotalLength ? shape.getTotalLength() : 100; shape.animate((ratio) => { return { lineDashOffset: -1 * ratio * length }; }, { duration: 2000, repeat: true }); } }, 'quadratic');lineDashOffset的负号表示流动方向,数值越大流动越快,数值越小则越缓慢。流动边适合表达实时建连、数据请求、消息推送等场景。flowInfoEdge则在流动边基础上增加label配置,它继承自流动边,在draw方法里额外添加一个文本图形。文本可以是链路名称、吞吐量或者最近一条告警消息。
注意:不要给所有边都加动画。拓扑图里边数量一多,动画会占据大量主线程。我通常会把流动边数量控制在 50 条以内,超过 50 条时改用静态虚线加箭头。
3.3 警告流边:颜色与状态的联动
警告流边是flowWarnEdge,它的核心是状态切换。正常时它是灰色静态边,当链路发生抖动时,通过graph.setItemState(edge, 'warning', true)让边变成红色并启动闪烁。实现时覆写getStateStyle比手动改 attr 更稳妥,因为 G6 的内部状态机在状态清除时会自动恢复原样式。
G6.registerEdge('flowWarnEdge', { getStateStyle(name, value, item) { const style = {}; if (name === 'warning' && value) { style.stroke = '#ff4d4f'; style.lineWidth = 3; style.lineDash = [6, 4]; } return style; }, afterDraw(cfg, group) { const shape = group.find(e => e.get('name') === 'keyShape'); if (shape) { shape.animate((ratio) => { return { lineDashOffset: -ratio * 40, opacity: 0.6 + 0.4 * Math.sin(ratio * Math.PI * 2) }; }, { duration: 800, repeat: true }); } } }, 'quadratic');这里的getStateStyle会在状态变化时自动调用,不需要手工刷新。实际项目中,警告流边还要响应节点警告状态,比如当目标节点进入warning状态时,所有连到它的边立即变红。这个逻辑放在菜单行为那一章再细化。quadraticLabelEdge则更多是布局层面的调整,它把贝塞尔曲线的控制点向上偏移一段距离,避免两条方向相反的边重叠在一起,然后在中点放置第二个标签。
边类型与参数速查表如下。
| 边类型 | 核心图形 | 动画方式 | 典型场景 |
|---|---|---|---|
| flowEdge | 虚线路径 | lineDashOffset 偏移 | 数据流通量展示 |
| flowInfoEdge | 虚线路径 + label | 流动 + 文本 | 带日志信息的数据流 |
| flowWarnEdge | 实线/虚线切换 | 闪烁 + 偏移 | 链路质量下降告警 |
| quadraticLabelEdge | 二次贝塞尔曲线 | 无动画 | 双向链路同时存在时 |
提示:二次标签边最容易被误读为普通边,在注册时一定要设置style.endArrow: true,并且给标签设置background和padding,否则文字会直接压在线上难以阅读。
4. 自定义右键菜单:从行为到交互闭环
4.1 事件绑定与菜单渲染
G6 提供node:contextmenu、edge:contextmenu和canvas:contextmenu事件。内置的 Menu 插件适合简单场景,但做网络拓扑时我更倾向于手写一个 DOM 菜单,因为菜单项需要跟节点类型、权限、甚至告警状态联动。具体做法是在初始化时给graph.on('node:contextmenu', handler)绑定事件,在handler里阻止浏览器默认菜单并渲染自定义 div。
graph.on('node:contextmenu', (evt) => { evt.preventDefault(); const item = evt.item; const { x, y } = evt.canvasPoint; const type = item.getModel().type; const menu = buildMenu(type, item); showMenuAt(menu, x, y); }); graph.on('edge:contextmenu', (evt) => { evt.preventDefault(); const item = evt.item; const { x, y } = evt.canvasPoint; const model = item.getModel(); const menu = buildEdgeMenu(model, item); showMenuAt(menu, x, y); });evt.canvasPoint是鼠标在 canvas 坐标系中的位置,因为画布可能被平移缩放,直接使用evt.clientX/clientY会导致菜单定位错位。buildMenu根据节点类型返回菜单项数组,比如缩放节点可以显示"固定当前大小",警告节点显示"查看告警详情",播放警告节点显示"停止播放动画"。
4.2 菜单项按节点/边类型动态生成
下面的buildMenu就是一个典型的策略分发,把不同类型对应到不同操作,同时允许外部传入一个contextMenuConfig来覆盖默认行为。插件包里的self-contextmenu.gif展示的正是这个流程。
const DEFAULT_NODE_MENUS = { zoomNode: [ { label: '定位到设备', action: 'locate' }, { label: '调整缩放比例', action: 'resize' } ], warningNode: [ { label: '标记已处理', action: 'resolve' }, { label: '设为播放警告', action: 'play' } ], playWarnNode: [ { label: '停止告警动画', action: 'stop' }, { label: '转为警告节点', action: 'toWarning' } ] }; function buildMenu(type, item) { return DEFAULT_NODE_MENUS[type] || [{ label: '节点详情', action: 'detail' }]; }这里返回的是菜单配置,真正的执行逻辑放在事件回调里。菜单项的意义不只是展示文字,还要告诉使用者这个操作会改变画布上的什么。如果某个菜单项不适用于当前节点,比如对playWarnNode显示"调整缩放比例",用户会感到困惑。所以策略表要尽量和节点类型一一对应,而不是共用一套通用菜单。
4.3 菜单行为与图形状态联动
菜单不能只是展示,点击后必须改变图形状态。这里要注意:graph.setItemState只能更新 G6 内部状态,如果要改数据里的业务字段,最好先graph.updateItem(item, { ...model, warning: false }),再调用setItemState刷新样式。否则下次重绘时又会回到旧状态。以"标记已处理"为例,点击后应该把节点从警告节点切换成普通节点。
function resolveWarning(item) { const model = item.getModel(); const updated = { ...model, type: 'zoomNode', warning: false }; graph.updateItem(item, updated); graph.setItemState(item, 'warning', false); graph.emit('after-resolve-warning', { item }); }这里的逻辑是:先更新数据,然后清除 G6 状态,最后触发自定义事件方便外部埋点和联动。如果直接修改 model 而不updateItem,画布上不会有任何变化;如果只setItemState而不改 model,刷新页面后警告状态会复活。
右键菜单的行为联动还可以延伸到边上:右键一条flowWarnEdge,可以选择"忽略告警",此时边从红色恢复为默认色。实现方式与节点一致,只需要在edge:contextmenu中把action分发到对应的处理函数。为了避免菜单覆盖整张图,右键菜单一定要在 canvas 外点击或滚动时自动隐藏,可以监听canvas:click和canvas:wheel来销毁当前菜单实例。
| 操作场景 | 触发对象 | 菜单动作 | 底层调用 |
|---|---|---|---|
| 设备负载查看 | zoomNode | 调整缩放比例 | updateItem + refresh |
| 告警处理 | warningNode | 标记已处理 | updateItem + setItemState |
| 告警降噪 | playWarnNode | 停止告警动画 | stopAnimate + setItemState |
| 链路误报 | flowWarnEdge | 忽略告警 | clearState + updateItem |
5. 动画性能、排错与验证技巧
最后落到实际开发上。这个插件包虽然开箱即用,但真正要把动画和菜单用稳,还得掌握下面几个技巧。
第一,动画不要全局开着。G6 中动画分布在节点和边的afterDraw里,属于持续占用渲染帧的类型。对于超过 300 个节点或 500 条边的拓扑图,我会在初始化时传animate: false,然后把要展示动态效果的部分单独标记为playWarnNode或flowEdge,只给这部分启动动画。需要批量停止时,使用graph.stopAnimate(true),但它会停掉所有动画,包括正在播放的警告动画。更好的做法是在插件内部维护一个动画 ID 列表,点击菜单里的"停止告警动画"时,只对目标节点执行:
const item = graph.findById(targetId); const group = item.getContainer(); group.find(e => e.get('name') === 'warn-ring').stopAnimate();第二,动画不生效时先检查三件事:注册名和数据里的 type 是否一致;draw/afterDraw是否返回了keyShape;group.animate是否在afterDraw里重复调用导致覆盖。最常见的是把animate写在了draw里,而draw只执行一次,真正改尺寸时不会重新触发动画。另外,如果节点被graph.hideItem隐藏再显示,动画实例会丢失,需要重新调用graph.refreshItem(item)。
第三,右键菜单定位问题,在与 G6 缩放交互并存时尤其明显。由于我们使用了canvasPoint定位,画布缩放后 DOM 菜单不会跟着放大,所以菜单字体还是用固定像素比较稳妥。如果画布被drag-canvas拖动,还要在canvas:drag开始前关闭菜单。
验证方法可以写成一段简单脚本:遍历所有节点,检查每个zoomNode的半径和数据中的 value 是否成正比,检查warningNode是否存在红色描边。这部分在 CI 里跑一遍能快速暴露组件在数据更新后的回归问题。
graph.getNodes().forEach(node => { const model = node.getModel(); if (model.type === 'zoomNode') { const key = node.getKeyShape().attr(); const expectedR = normalize(model.value) * 32; if (Math.abs(expectedR - key.r) > 0.1) { console.warn(`zoomNode ${model.id} 尺寸未同步`); } } });最后一个小技巧:把右键菜单的行为抽成一个纯函数,不直接操作 graph,而是返回要执行的命令。这样菜单的单元测试就变成了命令队列的测试,属于能从项目里带走的东西。
本文还有配套的精品资源,点击获取