「AntV」X6开发实践:踩过的坑与解决方案

2,300 阅读1分钟

长期更新版文档请移步语雀(「AntV」X6开发实践:踩过的坑与解决方案 (yuque.com))

🖼️ | 如何自定义拖拽源?

相信你们在开发中更多的需求是需要自定义拖拽源,毕竟自定义的功能扩展性高一些,而且可以根据你的业务需求灵活设置。自定义拖拽的优点就是:万物皆可成为拖拽源,不管你使用的是html标签,还是第三方的ui框架,或者树形列表,……这些都可以设置成拖拽源,只有你想不到的,没有官方做不到的,来吧,开整。

官方提供的拖拽

image.png

自定义的拖拽

image.png

解决方案

这里使用的是Dnd插件,因为我看官方介绍说Stencil插件内部也是依靠Dnd实现的,索性就直接使用Dnd来搞吧

步骤1:初始化Dnd

// 先定义个全局的dnd变量
let dnd = null;
// 在mounted中对dnd进行初始化(在graph之后初始化)
dnd = new Dnd({
  target: graph,
  scaled: false,
  dndContainer: proxy.$refs.dndContainer
});

步骤2:定义拖拽事件

/**
 * 自定义拖拽源事件
 * @param {*} e
 * @param {*} treeNode 根据需要传入要添加的参数
 * @param {*} data 根据需要传入要添加的参数
 * 这里使用的是elementPlus的tree组件
 */
const startDrag = (e, treeNode, data) => {
  console.log('eee', e);
  console.log('treeNode', treeNode);
  console.log('data', data);
  const node = graph.createNode({
    shape: 'cu-data-node',
    width: 150,
    height: 104,
    label: data?.label,
	// 传递给自定义节点的数据
    data: {
      label: data?.label,
      img: data?.img,
      desc: data?.desc
    },
    ports: {
      ...port,
      items: [
        {
          group: 'top'
        }
      ]
    }
  });
  dnd.start(node, e);
};

步骤3:自定义html节点

  // 注册自定义节点 图标+标题+描述
  Shape.HTML.register({
    shape: 'cu-data-node',
    width: 'auto',
    height: 104,
    effect: ['data'],
    html(cell) {
      // 获取节点传递过来的数据
      const { label, img, desc } = cell.getData();
      // 创建自定义的节点容器
      const container = document.createElement('div');
      container.setAttribute('class', 'cu-container');
      // 图片根据不同的类型进行切换,可以是后端返回的图标,也可以是自己本地的图标,如果是后端返回就通过节点的data传进来
      const container_img = document.createElement('img');
      container_img.src = currentTab.value === 0 ? '/src/assets/images/operator/datasouce.png' : img;
      container_img.setAttribute('class', 'cu-container-img');


      const container_title = document.createElement('div');
      container_title.innerText = label;
      container_title.setAttribute('class', 'cu-container-title');


      const container_desc = document.createElement('div');
      container_desc.setAttribute('class', 'cu-container-desc');
      container_desc.innerText = desc || '描述信息';


      container.appendChild(container_img);
      container.appendChild(container_title);
      container.appendChild(container_desc);


      return container;
    }
  });

步骤4:元素绑定拖拽事件

<!-- $event必传,后面的参数根据你的业务需求动态添加 -->
<div  @mousedown="startDrag($event, node, data)">拖拽的节点</div>

🖼️ | 本地图片导出后不显示?

先看看官方的导出文档:
图片导出

由于业务需要,需要把画布上的节点保存成图片供其它模块展示,如果你的后端返回的数据格式是前端想要的,那么大不必搞图片的形式,直接把官网的快速上手代码拿过来循环一下就好了……,这里就拿toPng的方法来讲解

问题梳理

  • 调用toPng拿到画布的base64数据
  • 把base64的数据传给后端
  • 后端把base64的数据转存后生成可访问的图片地址再返回给前端
  • 前端开始展示

然而事情却没有这么简单,第一步就遇到了一堆的坑,由于官网上导出的都是它内置的节点,所以导出都没啥问题,但是我使用的是html节点,导出的时候,我节点的图片就死活导不出来,而且导出的样式也是乱的(样式错乱问题)

解决方案

一句话:图片必须得是base64格式的导出才会有图片,不然无法导出节点的图片

  1. 自定义html节点中把图片转换成base格式的

需要调用 imageToDataUri把图片地址转成base64的数据,这个方法我也是摸索了好久才找到的,官方文档完全没有提及这个方法,如果需要查看其它方法,请打印 DataUri这个对象

DataUri.imageToDataUri('/images/operator/datasouce.png',
  function (nu, url) {
    // 第一个参数无效,用的只是第二个参数,但是第一个参数不写不行
    container_img.src = url; // 给图片标签赋值
  }
);
  1. 调用toPng生成base64数据

这个步骤中要处理的问题:导出后样式不正确,导出的时候页面闪动

graph.toPNG(
  dataUri => {
    console.log('dataUri >>>>', dataUri); // 这个就是base的图片地址
  },
  {
    width: 526,
    height: 268,
    backgroundColor: 'rgba(25, 87, 121, 0.18)',
    quality: 1, // 图片质量 取值范围:0-1,默认0.92
    // copyStyles: false,
    // 自定义样式表,为了解决导出后节点样式丢失的问题,暂时官方还没有修复这个bug
    stylesheet: `
      .cu-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      }
      .cu-container-title {
      color: #d3e6f3;
      }
      .cu-container-img {
      width: 53px;
      height: 53px;
      margin-bottom: 4px;
      }
      .cu-container-desc {
      color: rgba(211, 230, 243, 0.7);
      margin-top: 3px;
      }
      .cu-container-title,
      .cu-container-desc {
      font-size: 14px;
      font-weight: 400;
      line-height: 20px;
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 1;
      }
    `
  }
);

:::warning Tip:图片导出样式和原节点的样式不一致问题:如果你遇到了这个问题,目前最好的方法是不断调整stylesheet中的样式,直到导出的样式和原节点的样式几乎一致即可。当然,如果你的甲方不重视页面交互,咱们完全可以使用copyStyles:true这个属性就行了,这样就不用设置 stylesheet了 :::


🖼️ | 神奇的图片边框

:::tips 昨天才把html节点中的图片转成base格式的,今天就发现一个用户体验的问题;那么是啥呢?就是我从左侧的树形菜单中拖拽节点的时候(鼠标按下也是同样问题),发现节点的图片区域那里会出现一个边框,持续时间不是很长,就几毫秒的时间,但是当你连续拖拽几个不同节点的时候就会发现这个边框竟然又消失不见了,如果此时重新进入页面,再开始拖动节点,图片的边框又出现了。 :::

问题梳理

  • 是否是官方节点自带的边框?如果是,配置项是什么?
  • 图片的默认边框是否被清除?还是外围元素的边框导致?
  • 图片的加载时机?
  • 是否是转base64的问题,毕竟在这之前没有这个情况发生…… :::tips 带着这些问题,第一时间去翻阅了官方文档,发现没有和这个问题相关的配置项,即使有设置了也不管用;然后就把问题抛到了交流群里面,发现压根没人回答……,那就只能开始第二个方案了:把图片的默认边框都去除掉,比如border,box-shadow,outline这些属性都去除掉了,发现还是没用;好吧,开始第三套方案:使用new Image处理图片的加载时机问题,嗯!不出意外的话,这个方法还是不行 :::

定位问题

经过上面四个方案的尝试后,我大概知道了问题的源头在哪边了,那就是我自定义html节点中图片地址赋值的地方,由于DataUri.imageToDataUri 这个方法是个异步执行的,所以才会导致在渲染的时候会出现短暂的视觉差

解决方案

  • 先给图片赋值个普通的地址(非base64的地址)
  • DataUri.imageToDataUri('随便写个参数名',url)的回调中再把图片的src替换成base64的
const container_img = document.createElement('img');
container_img.setAttribute('class', 'cu-container-img');
container_img.setAttribute('alt', '节点ico');
container_img.style.cursor = 'pointer';
// 先用远程图片地址给图片的src赋值,然后再重新赋值成base64的格式;这么做的目的就是解决节点拖拽到画布上会出现短暂的边框闪动问题,如果你要复现这个边框,可以把下面这一行代码注掉(不是必现)
container_img.src = img;
// 把图片转成base64方便存储到后端
DataUri.imageToDataUri(img, function (nu, url) {
// 第一个参数无效,用的只是第二个参数,但是第一个参数不写由不行
container_img.src = url;
});

🖼️ | 画布内容从接口获取数据后无法居中?

场景

前端需要把画布上的节点保存到后端,然后前端在获取详情接口的时候要把节点进行居中处理

问题梳理

在不调用后端接口的情况下使用centerContent()是没得问题的;但是在动态获取节点数据后就会存在异步加载的问题,也就是先将内容居中了,之后再设置节点到画布中去,此时centerContent的时机已经过去了,节点还是更具自身的位置进行排列

解决方案

由于作者用的vue技术栈,所以这里的解决方法主要以vue为主

方案1:使用nextTick等待dom全部渲染完成

nextTick(() => {
	graph.centerContent();
});

方案2:直接在接口中使用

getDataView({ size: -1, name: item.tableMetaName }).then(res => {
    if (res.code === 0) {
      // 缩放
      graph.zoom(-0.1);
      // 画布居中
      graph.centerContent();
    }
});

🖼️ | 自定义右键菜单坐标不准确?

问题梳理

原本的写法是节点右键的时候通过node.position()的方法获取节点的坐标,然后再把节点的坐标绑到右键菜单的dom上,但是发现对画布进行平移的时候,右键菜单的位置还停留在第一次的位置,原因就是画布平移和节点没啥关系,节点的坐标并不会因为画布平移了就自动更改自身的坐标

解决方案

对鼠标的坐标进行转换,这也是1.9版本中新增的一个方法,关键是在官方文档中还找不到这个方法,只能死马当活马医了 坐标转换,果然问题解决了

graph.on('node:contextmenu', ({ e, node }) => {
  const pos = graph.clientToGraph(e.clientX, e.clientY);// 核心代码就是这一行
  createMenuDom({ x: pos.x, y: pos.y, node, type: 0 });
});

附:自定义右键菜单完整代码

这里需要对javascript的dom有点基础,不过这只是我创建dom的方法,如果你们想用其它的方法也是可以的哈

let divMenuContainer = null;
const createMenuDom = ({ x, y, node, edge, type }) => {
  if (divMenuContainer) {
    // 如果存在了菜单,就先移除再创建,不然你的页面上会多出来好多菜单的
    document.getElementById('container').removeChild(divMenuContainer);
  }
  divMenuContainer = document.createElement('div');
  divMenuContainer.setAttribute('class', 'div-menu-container');
  divMenuContainer.style.left = x + 30 + 'px';
  divMenuContainer.style.top = y + 'px';
  const divMenuItem = document.createElement('div');
  divMenuItem.setAttribute('class', 'div-menu-item');
  divMenuItem.innerText = type === 0 ? '删除节点' : '删除边';
  divMenuItem.addEventListener('click', () => {
    type === 0 ? graph.removeNode(node) : graph.removeEdge(edge);
    divMenuContainer.style.display = 'none';
  });
  divMenuContainer.appendChild(divMenuItem);
  document.getElementById('container').appendChild(divMenuContainer);
  document.body.addEventListener('click', () => {
    if (divMenuContainer) {
      divMenuContainer.style.display = 'none';
    }
  });
};
graph.on('node:contextmenu', ({ e, node }) => {
  // 坐标转换
  const pos = graph.clientToGraph(e.clientX, e.clientY);
  // 调用创建dom的方法,把坐标和节点信息传递进去
  createMenuDom({ x: pos.x, y: pos.y, node, type: 0 });
});

效果图

Snipaste_2023-06-10_17-14-30.png


🖼️ | 在历史记录中忽略某个属性的修改

常见问题
也是下面这个问题的解决方案
image.png
这是官方的demo
连线 undo - CodeSandbox

history插件配置

new Graph({
  history: {
    enabled: true,
    beforeAddCommand(event, args: any) {
      // 忽略历史变更
      if (args.options.ignoreHistory) {
        return false
      }
    },
  },
})

边的写法

graph.on('edge:connected', ({ edge }) => {
   // 传入自定义的 ignoreHistory 选项来忽略历史变更
   edge.attr('line/strokeDasharray', null, { ignoreHistory: true })
})

节点的写法

new History({
  enabled: true,
  beforeAddCommand(event, args) {
    if (args.options.ignoreHistory) {
      return false;
    }
  }
})
node.setData({ tableMeta: res.data.records, desc: res?.data?.records?.length || 0 }, { ignoreHistory: true });

🖼️ | x和y坐标为字符串报错问题

解决方案

直接把x,y坐标转成纯数字的即可,不然拖动节点的时候会报错的


🖼️ | 设置节点移动范围在画布内

Transform

new Graph({
  translating: {
  restrict: true
}
})

🖼️ | 获取当前节点的所有父级节点

此方法会返回所有的输入和输出边,如果只要输入边的节点或者输出边的节点信息,请看这里链接

const getParentNodes = node => {
  const nodeId = node.id;
  const connectedNodes = [];
  // 如果需要其他方法,请看下方的具体配置,根据自己的需要修改这里的代码即可
  const edges = graph.getConnectedEdges(node, { deep: true });
  for (const edge of edges) {
    const sourceNode = edge.getSourceNode();
    const targetNode = edge.getTargetNode();
    if (sourceNode.id !== nodeId) {
      connectedNodes.push(sourceNode);
    }
    if (targetNode.id !== nodeId) {
      connectedNodes.push(targetNode);
    }
  }
  return connectedNodes;
};

// 具体的配置
const edges = graph.getConnectedEdges(node) // 返回输入和输出边
const edges = graph.getConnectedEdges(node, { incoming: true, outgoing: true }) // 返回输入和输出边

const edges = graph.getConnectedEdges(node, { incoming: true }) // 返回输入边
const edges = graph.getConnectedEdges(node, { incoming: true, outgoing: false }) // 返回输入边

const edges = graph.getConnectedEdges(node, { outgoing: true }) // 返回输出边
const edges = graph.getConnectedEdges(node, { incoming:false, outgoing: true }) // 返回输出边

const edges = graph.getConnectedEdges(node, { deep: true }) // 返回输入和输出边,包含链接到所有子孙节点/边的输入和输出边
const edges = graph.getConnectedEdges(node, { deep: true, incoming: true }) // 返回输入边,包含链接到所有子孙节点/边的输入边
const edges = graph.getConnectedEdges(node, { deep: true, enclosed: true }) // 返回输入和输出边,同时包含子孙节点/边之间相连的边

const edges = graph.getConnectedEdges(node, { indirect: true }) // 返回输入和输出边,包含间接连接的边

🖼️ | 注册自定义节点报错?

报错信息

(in promise) Error: Node with name 'cu-port' already registered.

下面这个报错是群友发的,但是问题和我之前遇到的是一类问题,所以就直接告诉群友问题所在了😀
image.png

错误代码

Graph.registerNode(name,options)

正确代码

Graph.registerNode(name,options,true)


🖼️ | 常用方法集锦

  1. 清空画布:graph.clearCells()
  2. 画布缩小:graph.zoom(-0.5)
  3. 获取画布缩放比例:graph.zoom()
  4. 获取画布上所有节点:graph.getNodes()
  5. 获取画布上所有边:graph.getEdges()
  6. 设置节点移动范围在画布内链接
translating: {
  restrict: true
},
  1. 动态设置节点位置链接node.position(x,y)
  2. 节点只能被相同的源节点连接一次链接allowMulti: false
  3. 两个节点连接的时候获取目标节点的数据链接
graph.on('edge:connected', ({ isNew, edge, currentCell }) => {
  // 回调的参数:https://antv-x6.gitee.io/zh/docs/tutorial/intermediate/events/#%E8%BE%B9%E8%BF%9E%E6%8E%A5%E5%8F%96%E6%B6%88%E8%BF%9E%E6%8E%A5
  console.log('被连接的节点详细参数', currentCell);
  if (currentCell.data['type'] === 0) {
    proxy.$modal.msgError('数据源无法作为输出节点');
    // 移除连接的边
    graph.removeEdge(edge?.id);
  }
});
  1. 判断当前节点是否被连接链接
const node = graph.getCellById('node1')
const connectedEdges = graph.getConnectedEdges(node)

附:参考文档
Dnd插件
Stencil插件
图片导出
1.x常见问题
坐标转换
Transform
Model