「AntV」2. xflow自定义工具栏(自定义model、自定义命令)

1,212 阅读12分钟

注意:写文章的时候,我用的xflow版本是1.x的

1. 自定义位置

<CanvasToolbar
position={{ top: -ToolBarHeight, left: 0, right: 0, height: ToolBarHeight }}
/>

注意的是,这个位置是相对于XFlowCanvas组件位置的

<XFlowCanvas
    position={{ top: ToolBarHeight }}
>  
      <CanvasToolbar
        position={{ top: -ToolBarHeight, left: 0, right: 0, height: ToolBarHeight }}
      />
</XFlowCanvas>

上面的例子中,画布向下移动了ToolBarHeight像素,工具栏向上移动了ToolBarHeight像素。

2. 自定义工具栏元素

 <CanvasToolbar
     // 在config里面自定义
    config={toolbarConfig()}
  />
// toolbarConfig
import { createToolbarConfig } from '@antv/xflow';
import Toolbar from '../toolbar';

export default createToolbarConfig((config, proxy) => {
  config.setCustomToolbarRender(async (modelService, updateComponent) => {
    // Toolbar就是普通的react组件
    updateComponent(Toolbar);
    // 一定要返回toolbar
    return Toolbar;
  });
});

自定义工具栏后,你就丢失了xflow自带的功能按钮,你得自己实现一遍。

3. 自定义工具栏功能

3.1. 按钮切换 (自定义model的使用)

msedge_YIhKwgE1Pw.gif

这个编辑开关,做简单点就是组件内部的状态,复杂点就是组件外部也能控制是否可以编辑。

内部就不说了,就是setState。这里讲讲外部控制的思路,还记得第一章的时候那个数据流图吗,这里我们就用到model来存储改变可编辑状态。

3.1.1. 注册自定义model

自定义工具栏元素很难传递props,所以直接使用model来监听属性变化更方便。

<XFlow
    // 这个就是注册model的地方
    modelServiceConfig={modelServiceConfig()}
>
  <XFlowCanvas>
      <CanvasToolbar/>
  </XFlowCanvas>
</XFlow>
// modelServiceConfig

import { createModelServiceConfig, Disposable, DisposableCollection } from '@antv/xflow';
// 图只读状态
export const GraphReadOnlyModel = {
  id: 'GraphReadOnly',
  DefaultReadOnly: true,
};

// 大部分都是模板代码,不需要深究为什么这么写
// 要深究可以看看源码,就是对rxjs的一些操作封装了一下
export default createModelServiceConfig((config) => {
  config.registerModel((registry, graph) => {
    const list: Disposable[] = [
    // 主要就是这里
    // 这个数组可以注册很多和model,写法都是这样
      registry.registerModel({
          // id,唯一字符串就行
        id: GraphReadOnlyModel.id,
        // 初始值函数,用来设定初始值
        getInitialValue: () => GraphReadOnlyModel.DefaultReadOnly,
      }),
    ];
    const toDispose = new DisposableCollection();
    toDispose.pushAll(list);
    return toDispose;
  });
});

3.1.2. 获取model

XFlow内部的组件可以通过一些hook获取指定的model

useModelAsync函数用来获取model,同时监听model变化,返回一个state。等于是结合了modeluseState

// 获取xflowapp实例
  const app: IApplication = useXFlowApp();
// 返回[state, setState, model本身]
  const [graphReadOnly, _, graphReadOnlyModel] = useModelAsync({
  // 获取model的方法
  // 这个获取方法都是固定的
  // id就是上面我们定义的id
    getModel: async () => await app.modelService.awaitModel(GraphReadOnlyModel.id),
    // state初始值
    initialState: GraphReadOnlyModel.DefaultReadOnly,
  });

3.1.3. 使用model

使用就和普通的state一样,这里说一下怎么修改model

// useModelAsync返回的第三个参数就是model本身
// model里面有方法可以改变值
    graphReadOnlyModel.setValue(true);

结合上面三步,我们就可以在工具栏之外(但是要在XFlow上下文中)控制里面的状态。

3.2. 新建节点

msedge_aoJZkZWUDD.gif

很简单就是调用命令就行,

  const add = useCallback(async () => {
    const nodeId = numberId();
    const graph = await app.getGraphInstance();
    // 设置位置
    const { x, y } = graph.graphToLocal(100, 100);
    // 触发XFlowNodeCommands.ADD_NODE命令,这个executeCommand方法文档上有,就是执行命令的意思
    app.commandService.executeCommand<NsNodeCmd.AddNode.IArgs>(XFlowNodeCommands.ADD_NODE.id, {
    // 参数就是node的一些参数
      nodeConfig: {
        id: nodeId,
        label: '',
        renderKey: 'NODE',
        x,
        y,
        width: 180,
        height: 40,
        extra: { name: '新建节点', id: nodeId },
        ports: [
          {
            id: numberId(5),
            type: NsGraph.AnchorType.INPUT,
            group: NsGraph.AnchorGroup.TOP,
            tooltip: '输入',
          },
          {
            id: numberId(5),
            type: NsGraph.AnchorType.OUTPUT,
            group: NsGraph.AnchorGroup.BOTTOM,
            tooltip: '输出',
          },
        ],
      },
    });
  }, []);

3.2.1 节点位置

这里值得注意的是设置位置那里,如果不设置位置,当你移动画布之后新建节点的位置始终在固定的位置,而不是相对的位置。

拖动画布之后,新建节点位置还是以前的位置

graphToLocal的意思是,将画布坐标转化成画布本地坐标。

这里解释一下:

  1. 画布坐标:就是整个画布的坐标,它不考虑画布的平移缩放
  2. 画布本地坐标local,考虑了平移缩放,是一个相对值

如下图,红色框就是我们可以看到的部分,黑色框就是整个画布。现在我们拖动了画布,将画布往上移动了一点。图中的(0,0)坐标就是两种的区别。 image.png

所以为了,拖动画布的情况下,新建的节点仍然是在可见范围里面,我们需要指定节点位置。

msedge_KyTKIP2LH0.gif

3.3. 删除节点

选中节点后删除,可以支持多选删除。

msedge_y6cod4Hxs3.gif

  const del = useCallback(async () => {
  // 获取所有已经选择的cell
  // cell就是node和edge的父类
    const cells: Cell[] = await MODELS.SELECTED_CELLS.useValue(app.modelService);
    // 遍历删除
    await Promise.all(
      cells.map((cell) => {
        const isEdge = cell.isEdge();
        const isNode = cell.isNode();
        if (isEdge || isNode) {
        // 删除边和删除节点的参数不一样,所以需要判断一下
          return app.commandService.executeCommand(
            isEdge ? XFlowEdgeCommands.DEL_EDGE.id : XFlowNodeCommands.DEL_NODE.id,
            isEdge
              ? {
                  edgeConfig: { id: cell.id },
                }
              : {
                  nodeConfig: {
                    id: cell.id,
                  },
                },
          );
        }
        return [];
      }),
    );
  }, []);

3.3.1. 删除按钮根据选择判断激活

选择了节点之后,删除按钮才会激活。原理就是使用model监听选择节点,这个model是自带的。文档上有

  const [isNodeSelected] = useModelAsync({
    getModel: async () => await MODELS.IS_NODE_SELECTED.getModel(app.modelService),
    initialState: false,
  });

3.4. 群组操作

可以新建解散分组。

msedge_YejtFd18AU.gif

3.4.1. 新建群组

  const createGroup = useCallback(async () => {
    const cells = await MODELS.SELECTED_CELLS.useValue(app.modelService);
    const groupChildren: string[] = [];
    // 收集应该加入群组的节点
    cells.forEach((cell) => {
      if (cell.isNode()) {
        groupChildren.push(cell.id);
      }
    });
    // 新建群组命令
    app.commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(XFlowGroupCommands.ADD_GROUP.id, {
    // 这里配置的是群组节点
    // 群组其实也是一种节点
      nodeConfig: {
        id: numberId(),
        renderKey: 'GROUPNODE',
        groupChildren,
        groupCollapsedSize: { width: 200, height: 40 },
        label: '新建群组',
      },
    });
  }, []);

3.4.2. 解散群组

  const unGroup = useCallback(async () => {
    const cell = await MODELS.SELECTED_NODE.useValue(app.modelService);
    if (cell) {
      const nodeConfig = cell.getData();
      app.commandService.executeCommand<NsGroupCmd.AddGroup.IArgs>(
        XFlowGroupCommands.DEL_GROUP.id,
        {
          nodeConfig: nodeConfig,
        },
      );
    }
  }, []);

这里还有一个边界条件,能够解散的节点要判断一下是不是群组节点:

  const [isGroupSelected] = useModelAsync({
  // IS_GROUP_SELECTED是xflow自带的model
    getModel: async () => await MODELS.IS_GROUP_SELECTED.getModel(app.modelService),
    initialState: false,
  });
  

3.4.3. 折叠群组

这个功能xflow是有bug的,在嵌套群组的条件下,直接使用折叠命令会有显示问题。

  1. 嵌套元素没有隐藏完

msedge_AbC8qrDx4n.gif

翻看源码可以发现,原始的折叠代码只是隐藏了直接子节点,但是子节点是群组的情况,并没有再隐藏群组里面的节点。

所以我们需要自定义这个命令,也就是自己写命令。

自定义命令也是一堆模板代码,直接复制原始的COLLAPSE_GROUP命令代码修改。 下面代码大部分是复制的,看着有点复杂,但是只需要关注修改部分,修改部分有注释标注。

3.4.3.1. 命令实现类

import type { Cell, Graph, Node as X6Node } from '@antv/x6';
import {
  Disposable,
  IArgsBase,
  ICommandContextProvider,
  ICommandContributionConfig,
  ICommandHandler,
  IHooks,
  NsGraph,
} from '@antv/xflow';
import type { HookHub } from '@antv/xflow-hook';
import { inject, injectable } from 'mana-syringe';

interface IToggleGroupCollapseService {
  (args: ToggleCollapseGroupCommandArgs): Promise<boolean>;
}
// 命令的参数
export interface ToggleCollapseGroupCommandArgs extends IArgsBase {
  /** 折叠的group node id */
  nodeId: string;
  /** 是否折叠 */
  isCollapsed: boolean;
  /** 折叠后的大小 */
  collapsedSize?: { width: number; height: number };
  /** 间距 */
  gap?: number;
  /** 更新群组是否折叠的状态,返回false时不执行 */
  toggleService?: IToggleGroupCollapseService;
}
export interface ToggleCollapseGroupCommandArgsResult {
  err: null | string;
}
export interface ToggleCollapseGroupCommandArgsCmdHooks extends IHooks {
  collapseGroup: HookHub<ToggleCollapseGroupCommandArgs, ToggleCollapseGroupCommandArgsResult>;
}
type ICommand = ICommandHandler<
  ToggleCollapseGroupCommandArgs,
  ToggleCollapseGroupCommandArgsResult,
  ToggleCollapseGroupCommandArgsCmdHooks
>;

@injectable()
/** 添加子节点命令 */
export class ToggleCollapseGroupCommand implements ICommand {
  @inject(ICommandContextProvider) contextProvider: ICommand['contextProvider'];
   
  toggleVisible = (cells: Cell[], visible: boolean, graph: Graph) => {
    cells.forEach((cell) => {
      const view = graph.findViewByCell(cell)!.container as HTMLElement;
      view.style.visibility = visible ? 'visible' : 'hidden';
    });
  };
   //***************************** 自定义代码开始*****************************//
  // 递归获取子节点,并隐藏,这样才能获取正确的相对位置
  toggleChildrenCollapse(
    groupNode: X6Node,
    graph: Graph,
    args: ToggleCollapseGroupCommandArgs,
    // 是否在递归里面
    // 判断这个可以间接的知道,此时处理的是不是嵌套群组的子节点
    inRecursion = false,
  ) {
    const childrens = groupNode.getChildren()!.filter((n) => n.isNode()) as X6Node[];
    const { isCollapsed, gap = 0 } = args;

    if (childrens) {
      childrens.forEach((item) => {
        /**
            先隐藏群组的子节点
        */
        // 群组折叠除了隐藏节点和边,还需要记住原本的位置
        // 不然再次展开的时候位置就是错的
        // 这里的节点位置都是相对于父节点的位置,所以父节点不能比子节点先隐藏,不然获取的位置就是不正确的
        if (item.getData().isGroup) {
          this.toggleChildrenCollapse(item, graph, args, true);
        }
        /**
            再隐藏群组节点
        */
        const position = groupNode.position();
        // 获取需要隐藏的边
        let innerEdges = graph.getConnectedEdges(item).filter((edge) => {
          const sourceNode = edge.getSourceNode();
          const targetNode = edge.getTargetNode();
          // 如果在递归中(嵌套群组的子节点)
          // 那么所有的边都隐藏
          return inRecursion
            ? true
            // 否则就只隐藏群组内部节点之间的边
            : childrens.includes(sourceNode!) && childrens.includes(targetNode!);
        });
        // 需要折叠
        if (isCollapsed) {
          // 把边和节点隐藏
          this.toggleVisible([item, ...innerEdges], false, graph);
          // 把节点折叠之前的尺寸和相对位置都保存在他自己身上
          item.prop('previousSize', item.size());
          item.prop('previousRelativePosition', item.position({ relative: true }));
          
          // 下面是复制的
          // 估计是为了gap这个属性
          item.position(position.x + gap, position.y + gap);
          const size = groupNode.size();
          item.size({
            width: size.width - gap * 2,
            height: size.height - gap * 2,
          });
        } else {
            // 展开的时候
          this.toggleVisible([item, ...innerEdges], true, graph);
          const pos = item.prop('previousRelativePosition');
          const size = item.prop('previousSize');
          // 读取之前存储的位置和大小,重新设置上去
          // position第二个参数可以设置位置为相对位置
          item.position(pos.x, pos.y, { relative: true, deep: true });
          item.size(size);
        }
      });
    }
  }

  toggleCollapse = (groupNode: X6Node, graph: Graph, args: ToggleCollapseGroupCommandArgs) => {
    const groupData = groupNode.getData<NsGraph.INodeConfig>();
    const { isCollapsed, gap = 0 } = args;
    // 保存群组尺寸
    if (isCollapsed) {
      const collapsedSize = args.collapsedSize ||
        groupData.groupCollapsedSize || { width: 180, height: 36 };
      groupNode.prop('previousSize', groupNode.size());
      groupNode.size(collapsedSize);
    } else {
      groupNode.size(groupNode.prop('previousSize'));
    }
    
    // !!!!!!!!!!关键修改这里,增加了一个递归!!!!!
    // 隐藏子节点
    this.toggleChildrenCollapse(groupNode, graph, args);

    groupNode.prop('isCollapsed', isCollapsed);
    groupNode.setData({
      ...groupNode.getData(),
      isCollapsed,
    });
  };
   //***************************** 自定义代码结束*****************************//
   
   // 命令调用函数
  execute = async () => {
    const ctx = this.contextProvider();
    const { args, hooks: runtimeHook } = ctx.getArgs();
    const hooks = ctx.getHooks();

    const result = await hooks.collapseGroup.call(
      args,
      async (handlerArgs) => {
        const x6Graph = await ctx.getX6Graph();
        const node = x6Graph.getCellById(args.nodeId) as X6Node;
        const { toggleService } = handlerArgs;

        if (toggleService) {
          const canToggle = await toggleService(handlerArgs);
          if (!canToggle) return { err: 'service rejected' };
        }

        if (node) {
          this.toggleCollapse(node, x6Graph, args);
          ctx.addUndo(
            Disposable.create(async () => {
              if (node) {
                this.toggleCollapse(
                  node,
                  x6Graph,
                  Object.assign(args, { isCollapsed: !args.isCollapsed }),
                );
              }
            }),
          );
        }

        return { err: null };
      },
      runtimeHook,
    );
    ctx.setResult(result!);

    return this;
  };
    
   // 下面都是一些历史操作相关的,用来控制是否可以撤销恢复的逻辑
  undo = async () => {
    const ctx = this.contextProvider();
    ctx.undo();
    return this;
  };

  redo = async () => {
    const ctx = this.contextProvider();
    if (!ctx.isUndoable) {
      await this.execute();
    }
    return this;
  };

  isUndoable(): boolean {
    const ctx = this.contextProvider();
    return ctx.isUndoable();
  }
}

// 就是一些配置,比如id啥的方便之后使用
const ToggleCollapseGroupCmd: ICommandContributionConfig = {
  /** Command: 用于注册named factory */
  command: {
    id: 'xflow:toggle-collapseGroup',
    label: '折叠展开群组',
    category: '节点操作',
  },
  /** hook name */
  hookKey: 'toggleCollapseGroup',
  CommandHandler: ToggleCollapseGroupCommand,
};
export default ToggleCollapseGroupCmd;

这段代码有几个注意的点:

  1. toggleVisible方法中,隐藏节点使用的是设置visibility,而不是x6文档中的setVisible方法,因为setVisible是真的会移除元素,当有大量元素的时候,频繁移除重建元素会有点卡。
  2. 主要起作用的是toggleChildrenCollapse方法,这个方法不仅要处理子节点是群组的情况,还要注意节点原本的相对位置。

3.4.3.2. 注册命令

  <XFlow
    commandConfig={commandConfig()}
  ></XFlow>
// commandConfig
import { createCmdConfig, Disposable, DisposableCollection } from '@antv/xflow';
import ToggleCollapseGroup from '../commands/toggleCollapseGroup';

export default createCmdConfig((config) => {
  config.setCommandContributions(() => [ToggleCollapseGroup]);
});

3.4.3.3. 使用命令

因为我们自定义了群组折叠功能,所以我们需要自定义一个群组元素来调用这个命令

import { Cell } from '@antv/x6';
import { NsGraph, useXFlowApp } from '@antv/xflow';
import ToggleCollapseGroupCmd, {
  ToggleCollapseGroupCommandArgs,
} from '../../commands/toggleCollapseGroup';
const GraphGroupNode: NsGraph.INodeRender<NsGraph.INodeConfig> = (props) => {
  const cell: Cell = props.cell;
  const app = useXFlowApp();
  // 这个data就是命令里面setData设置的
  const isCollapsed = props.data.isCollapsed || false;
  
  // 假设某个按钮点击之后展开
  const onExpand = () => {
    app.executeCommand(ToggleCollapseGroupCmd.command.id, {
      nodeId: cell.id,
      isCollapsed: false,
      collapsedSize: { width: 200, height: 40 },
      gap: 3,
    } as ToggleCollapseGroupCommandArgs);
  };
  // 折叠
  const onCollapse = () => {
    app.executeCommand(ToggleCollapseGroupCmd.command.id, {
      nodeId: cell.id,
      isCollapsed: true,
      collapsedSize: { width: 200, height: 40 },
      gap: 3,
    } as ToggleCollapseGroupCommandArgs);
  };
  return (
    <div>
    
    </div>
  );
};

export default GraphGroupNode;

自定义节点使用文档上有,就是在graphConfig中注册一下

  // 群组节点
  config.setNodeRender('GROUPNODE', GroupNode);

3.4.3.4. 最简指令模板

上面的代码有点多,这里给一个模板,一个什么也不做的空指令

// 空的一个命令,可以用来在executeCommandPipeline执行任何命令
import type { HookHub, ICmdHooks as IHooks, ICommandContributionConfig } from '@antv/xflow';
import { ManaSyringe } from '@antv/xflow';

import type { IArgsBase, ICommandHandler } from '@antv/xflow';
import { ICommandContextProvider } from '@antv/xflow';

export interface LoopCommandArgs extends IArgsBase {
  loop: (ctx: ReturnType<ICommand['contextProvider']>) => any;
}
export interface LoopCommandResult {
  [key: string]: any;
}
export interface LoopCommandHooks extends IHooks {
  loop: HookHub<LoopCommandArgs, LoopCommandResult>;
}
type ICommand = ICommandHandler<LoopCommandArgs, LoopCommandResult, LoopCommandHooks>;

@ManaSyringe.injectable()
/** 部署画布数据 */
export class LoopCommand implements ICommand {
  @ManaSyringe.inject(ICommandContextProvider) contextProvider: ICommand['contextProvider'];
  /** 执行Cmd */
  execute = async () => {
    const ctx = this.contextProvider();
    const {
      args: { loop },
    } = ctx.getArgs();
    await loop?.(ctx);
    return this;
  };

  /** undo cmd */
  undo = async () => {
     //const ctx = this.contextProvider();
    //ctx.undo();
    return this;
  };

  /** redo cmd */
  redo = async () => {
     //const ctx = this.contextProvider();
    //if (!ctx.isUndoable) {
    //  await this.execute();
   // }
    return this;
  };

  isUndoable(): boolean {
    const ctx = this.contextProvider();
    return ctx.isUndoable();
  }
}

const LoopCmd: ICommandContributionConfig = {
  command: {
    id: 'xflow:loop',
    label: '空操作',
    category: '空操作',
  },
  // 这个命令hook的配置
  /** hook name */
  hookKey: 'loop',
  CommandHandler: LoopCommand,
};

export default LoopCmd;

3.5. 导出图片

建议使用自带的api,不要用htmnl2canvas之类的,因为x6的元素都是svg,而且自定义元素使用了foreignObject标签,导出有点麻烦。

  const exportImage = useCallback(async () => {
    const dataUri = await exportGraph2PNG(app);
    const a = document.createElement('a');
    a.href = dataUri;
    a.download = `文件名.png`;
    a.click();
  }, []);
// exportGraph2PNG
export function exportGraph2PNG<T extends boolean>(
  app: IApplication,
  options?: ExportGraph2JPGOptions<T>,
): Promise<Blob | string> {
// toPNG有回调函数,所以用promise包了一下
  return new Promise((resolve) => {
    app.getGraphInstance().then((graph) => {
      graph.toPNG(
          // 参数1,回调返回的是一个datauri
          // 类似data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAF
        (dataUri: string) => {
          if (options?.toBlob) {
          // DataUri是x6自带的工具类
          // 把datauri转化为blob方便下载
            resolve(DataUri.dataUriToBlob(dataUri));
            return;
          }
          resolve(dataUri);
        },
        // 参数2是一些配置,具体可以看看x6官网
        // 大概就是设置导出图像的显示范围,一般就是全部
        {
          preserveDimensions: {
            width: document.body.clientWidth,
            height: document.body.clientHeight,
          },
          viewBox: {
            x: 0,
            y: 0,
            width: document.body.clientWidth,
            height: document.body.clientHeight,
          },
          // 这是在生成图像之前做处理
          beforeSerialize(svg) {
            // 比如我可以删掉群组节点的展开按钮
            const groupNodeSpanIcon = svg.querySelectorAll(
              'div[class*="xflow-group-node"] span[role="img"]',
            );
            groupNodeSpanIcon.forEach((ele) => ele.remove());
          },
        },
      );
    });
  });
}

3.6. 保存图数据

很常见的功能就是保存图数据上传至后端

  const saveData = useCallback(async () => {
    setLoading(true);
 app.commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
      XFlowGraphCommands.SAVE_GRAPH_DATA.id,
      {
        saveGraphDataService: async (meta, graphData) => {
          try {
            // 请求后端
          } catch (err) {
            console.log(err);
            message.error('保存失败');
          } finally {
            setLoading(false);
          }
        },
      },
    );
  }, []);

但是这样的话不好封装组件,这样相当于写死在toolbar组件里面了,不同页面请求的接口参数都不一样所以我想了一个方法,把回调函数放进x6配置里面,在这里面调用。其实相同的思路可以放在其他地方。

// 封装的Graph组件
// 接受参数onSaveData
const Graph = ({onSaveData}) => {
    return <XFlow>
        <XFlowCanvas
          // 传进去的参数会作为proxy的value
          config={getGraphConfig({ onSaveData })}
        >
    </XFlow>
}
// getGraphConfig

// createGraphConfig函数返回的是一个函数,这个函数的参数可以使用proxy获取
// 其实xflow里面的配置都可以这样传参数
const getGraphConfig = createGraphConfig<GetGraphConfigProps>((config, proxy) => {
  /** 设置画布配置项,会覆盖XFlow默认画布配置项 */
  config.setX6Config({
    // 注入的额外配置
    // @ts-ignore
    onSaveData: proxy.getValue().onSaveData,
  });
});
// toolbar

  const saveData = useCallback(async () => {
  // 获取x6配置,获取我们传进来的回调函数
    const {
      x6Options: {
        // @ts-ignore
        onSaveData,
      },
    } = await app.getGraphConfig();
    app.commandService.executeCommand<NsGraphCmd.SaveGraphData.IArgs>(
      XFlowGraphCommands.SAVE_GRAPH_DATA.id,
      {
        saveGraphDataService: async (meta, graphData) => {
          try {
          // 调用回调
            await onSaveData?.(app, meta, graphData);
          } catch (err) {
            console.log(err);
          } finally {
          }
        },
      },
    );
  }, []);

3.7. 复原

不小心操作了图,或者想重新来,这时候复原功能就很有用

msedge_ExJEdX7vX3.gif

复原直接重新设置数据是不够的,因为展示的数据还有额外的布局信息,如果没有布局信息,复原的就是乱的。

我的思路是:

  1. 在渲染完成之后保存图数据
  2. 复原的时候重新加载这些数据

3.7.1 保存节点变数据

要做到这一点,你得知道什么时候渲染完成,所以我不让XFlow自动渲染而是手动渲染。

// 自定义一个model用来保存初始数据
import { createModelServiceConfig, Disposable, DisposableCollection } from '@antv/xflow';
// 图默认数据
export const DefaultGraphDataModel = {
  id: 'DefaultGraphDataModel',
};
export default createModelServiceConfig((config) => {
  config.registerModel((registry, graph) => {
    const list: Disposable[] = [
      registry.registerModel({
        id: DefaultGraphDataModel.id,
        getInitialValue: () => null,
      }),
    ];
    const toDispose = new DisposableCollection();
    toDispose.pushAll(list);
    return toDispose;
  });
});

// 原本只要给XFlow组件传递graphData属性就能自动渲染,但是这里为了手动渲染就不传
  useEffect(() => {
      // 从后端请求回来了数据
    if (graphData) {
    // executeCommandPipeline就是流水线模式执行一系列命令
    /**
        [
            {
                命令id,
                参数: {
                    命令id,
                    async getCommandOption(ctx) {
                        return 命令参数
                    }
                }
            }
        ]
    */
      graphApp.current?.commandService.executeCommandPipeline([
        // 设置渲染
        {
          commandId: XFlowGraphCommands.GRAPH_RENDER.id,
          async getCommandOption(ctx) {
            return {
              commandId: XFlowGraphCommands.GRAPH_RENDER.id,
              args: {
                graphData,
                // 节点渲染完成后的回调
                // 同样的功能还可以在render的hook之后
                // 但是hook触发的时机不对,并不能保证真的渲染完成
                // 这个可以参考GRAPH_RENDER命令的源码
                afterRender(graphData, graphMeta) {
                  const { nodes, edges } = graphData;
                  // 创建命令列表
                  const groupCmdList: IGraphPipelineCommand[] = [];

                  // 所有渲染完成后保存初始数据
                  groupCmdList.push({
                      // 这个LoopCmd就是前面那节创建的一个空的命令
                    commandId: LoopCmd.command.id,
                    async getCommandOption(ctx) {
                      return {
                        commandId: LoopCmd.command.id,
                        args: {
                          async loop(ctx) {
                            const graphData = await graphApp.current?.getGraphData();
                            // 保存初始数据
                            const defaultGraphDataModel =
                              await graphApp.current?.modelService.awaitModel(
                                DefaultGraphDataModel.id,
                              );
                            if (defaultGraphDataModel) {
                              defaultGraphDataModel.setValue(graphData);
                            }
                          },
                        } as LoopCommandArgs,
                      };
                    },
                  });
                  graphApp.current?.commandService.executeCommandPipeline(groupCmdList);
                },
              } as NsGraphCmd.GraphRender.IArgs,
            };
          },
        },
        // 设置布局
        {
          commandId: XFlowGraphCommands.GRAPH_LAYOUT.id,
          async getCommandOption(ctx) {
            return {
              commandId: XFlowGraphCommands.GRAPH_LAYOUT.id,
              args: {
                graphData,
                ...graphLayout,
              },
            };
          },
        },
        // 设置缩放
        {
          commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
          async getCommandOption(ctx) {
            return {
              commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
              args: {
                factor: 'fit',
                zoomOptions: CANVAS_SCALE_TOOLBAR_CONFIG.zoomOptions,
              } as NsGraphCmd.GraphZoom.IArgs,
            };
          },
        },
      ]);
    }
  }, [graphData]);

3.7.2. 重新加载数据

// 获取原始数据model
const [defaultGraphData] = useModelAsync({
    getModel: async () => await app.modelService.awaitModel(DefaultGraphDataModel.id),
    initialState: null,
  });
// 重置
const resetData = useCallback(async () => {
    if (defaultGraphData) {
      app.commandService.executeCommandPipeline([
        {
        // 重新渲染
          commandId: XFlowGraphCommands.GRAPH_RENDER.id,
          async getCommandOption(ctx) {
            return {
              commandId: XFlowGraphCommands.GRAPH_RENDER.id,
              args: {
                graphData: defaultGraphData,
              } as NsGraphCmd.GraphRender.IArgs,
            };
          },
        },
        {
        // 缩放
          commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
          async getCommandOption(ctx) {
            return {
              commandId: XFlowGraphCommands.GRAPH_ZOOM.id,
              args: {
                factor: 'fit',
                zoomOptions: CANVAS_SCALE_TOOLBAR_CONFIG.zoomOptions,
              } as NsGraphCmd.GraphZoom.IArgs,
            };
          },
        },
      ]);
    }
  }, [defaultGraphData]);