简介

XFlow 是 AntV 旗下, 基于 X6 图编辑引擎、面向 React 技术栈用户的图编辑应用级解决方案, 旨在让复杂的图编辑应用开发简单高效。
类比antd体系, X6 是图编辑场景的 antd, 提供图编辑的各种原子能力。而 XFlow 是图编辑场景的 ProComponent, 通过 App 扩展系统/状态管理/命令模式来实现对 X6 的原子能力的组合封装, 最终实现应用级场景的开箱即用。
XFlow 源自蚂蚁体验技术部数据智能团队, 已经在蚂蚁大数据部、人工智能中台业务场景深度打磨验证, 值得信赖!
如果您还没有使用过 XFlow, 建议通过 快速上手 抢先体验 XFlow 的魅力。

💡 命名由来

XFlow 的 X 代表从属于 X6 体系, Flow 可以理解为流程图也可以理解使用起来像水一样流畅、易用、可扩展。

✨ 特性

🌱 极易定制:支持使用 React 组件 开发节点/连线样式。
🚀 开箱即用:内置 1 个快速上手 + 3 个解决方案, 内置若干 React 交互组件, 如小地图、对齐线、右键菜单等。
💯 生产可用:源自蚂蚁体验技术部数据智能团队, 在蚂蚁大数据部、人工智能中台业务场景深度打磨验证。
🧲 一切皆可扩展:内置统一的扩展模式, 可按照自己的业务需求扩展图交互, 所有组件皆可扩展。

技术储备

React

XFlow 的一个特点是一切都是 React 组件,准确的说是 React 函数组件,花一些时间熟悉 React hooks,会让你更容易理解代码。在全局状态管理方面,XFlow 并没有引入像 redux 这样的第三方库,而是采用 React 自带的 React Context

mana-syringe

在 XFlow 里面大量使用了 mana-syringe,它是一个开箱即用的 IOC-container。不过 mana-syringe 本身其实并没有太多东西,它其实是对 inversify 的封装。建议这两个官方文档都好看看。
当然如果你像我当初一样,连 IOC-container 是个啥都不知道的话,建议先去了解一下这个概念。IOC- container 的话,简单来讲是为了在复杂程序中通过容器来集中处理依赖关系,它是基于 DIP 原则设计的。下面推荐几篇文档,都是我当初学习的时候看过的:
英文博客,比较详细地介绍了以上几个概念,代码 demo 写的也很好。
https://viktor-kukurba.medium.com/dependency-injection-and-inversion-of-control-in-javascript-303e07e7f43f
知乎上的,感觉质量稍差一些,代码 demo 不是很好理解,不过是中文写的,相比之下更易读,可以对照着看
https://zhuanlan.zhihu.com/p/61018434

RxJS

RxJS 拥有 26.6k 的 star,也算是个明星项目吧
image.png
不过它在 XFlow 里面用得不多,只是 ModelService 的底层依赖。RxJS 是一个使用可观察序列编写异步和基于事件的程序的库。 它提供了一种核心类型,即 Observable、卫星类型(Observer、Scheduler、Subjects)和受 Array#extras 启发的操作符(map、filter、reduce、every 等),以允许将异步事件作为集合处理。你可以把它理解为事件的 Lodash
不过真正在 XFlow 里面用到的只有以下几个 api,赶时间的先看一下它们,当然你也可以选择暂时不看…,反正不影响整体阅读,不过不懂 IOC-container 的话,我打赌你源码绝对会看得心态爆炸。
image.png

immer

immer 是一个非常轻量级的库,它的作用很简单,就是提供一些 api,让我们可以不更改源对象的任何属性,而是始终创建一个更改后的副本。
可以看下面例子或者官网简单理解一下
image.png
反正这东西在 XFlow 里面用到的不多
image.png

设计模式

XFlow 里面随处可见设计模式的影子,比如单例模式、命令模式、发布-订阅模式(又称观察者模式)等。关于设计模式可以看一下曾探写的 《JavaScript设计模式与开发实践》

心得总结

React 自不必多说,mana-syringe 是极为重要的,整个 XFlow 核心层的架构都用到了它,要想理解它,需要了解很多抽象的概念,不过也不用着急马上看懂,可以边阅读 XFlow 的源码,边学习。至于 RxJS 、 immer 和设计模式就随缘看看吧。

目录结构

讲完技术储备,就先来看看目录结构吧。XFlow 采用了多包的管理模式,/XFlow/packages 下有五个子包,分别是 xflow、xflow-core、xflow-docs、xflow-extension、xflow-hook。下面分别简单介绍一下。
image.png

xflow

这东西比较简单,其实就是收集其他子包的 api 和组件,并向外暴露,让用户可以调用。
image.pngimage.png

xflow-core

顾名思义,xflow-core 是 XFlow 的核心部分,它向外暴露 XFlow 最为核心的 XFlow 工作台组件XFlowCanvas 画布组。同时提供了 XFlowCommandsMODELSXFlow GraphProviderXFlow Hooks 等核心 API。

xflow-docs

这个其实就是我们的官方文档以及官网上 demo。
image.png
像官网上的 DAG 解决方案ER建模解决方案流程图解决方案 的代码都在这里面。

xflow-extension

它是 XFlow 的拓展部分,对外提供了一些 UI 交互组件,能够帮助我们快速搭建图编辑项目。可以看一下它的目录,最下面的flowchart-canvas、flowcart-editor-panel、flowchart-node-panel、flowchat-toolbar 是我们专门为 流程图解决方案 定制的组件,功能强大,开箱即用。
image.png

xflow-hook

在 XFlow 中扩展逻辑都是通过 Hook 来完成,XFlow 内部可以注册 Hook 逻辑来完成对 Graph 配置和 Command 的扩展。在 index.ts 和 utils.ts 中定义了 hooks 执行的策略和顺序。具体会在 CommandService 里面讲到,单独讲比较抽象。
image.png

项目启动与开发

启动与监听

这个直接看 GitHub 仓库的介绍吧,安装后在根目录通过 yarn start 就可以运行项目,项目运行起来就是我们的官网,默认端口是 8000
image.png
如果要开发,记得要进入相应的子包下通过 yarn start 开启监听。

  • cd packages/xflow-extension
  • yarn start

之后改动 xflow-extension 中的代码,就会呈现热更新到页面上了。

less

XFlow 中的样式使用了 less,但是它们不是通过直接引入生效的,而是会统一打包到 dist/index.css 目录下, 因此如果改动样式后需要通过先使用命令 yarn build:less 后才能生效。此外,切换分支的时候,最好也先 yarn build:less 因为,打包后的 css 文件不会被 git 管理。

xflow-core 核心代码解读

项目入口

在 XFlow 中, 一切都是 React 组件。XFlow 工作台组件是 XFlow 的核心组件之一, 可以理解为是一个图编辑应用的工作空间, 它包含了画布组件、各种交互组件等。而项目的入口就是我们的 xlow 工作台组件,而前面提到的 XFlowCanvas 画布组 XFlowCommandsMODELSXFlow GraphProviderXFlow Hooks,都可以理解为它的子组件。
image.png

XFlowAppInternalProvider

它处于 React 组件树的最外层,是一个 context 组件,作用主要是为子组件提供 XFlow 的实例 App
可以去目录里面瞅一眼
这里最重要的就是向外暴露了一个 useXFlowApp 的自定义 hooks,使我们能够在 XFlow 的子组件下获取 app 实例
image.png

注意!这是个 hooks,只能在组件里面用,而且是基于 React context 的,所以只能是 XFlow 组件的子组件

App 实例

可以看一下它的 interface,对应的目录是 packages/xflow-core/src/xflow-main/application.ts
这是个很重要的对象,从它身上可以获取 commandService、modelService以及画布实例与配置。
image.png

ExtensionRegistryContext.Provider

它位于组件树的第二层,看起来也是一个 React Context,它提供了一个 extensionRegistry,用于收集 commandService 等拓展配置,并统一注入给 app 实例。
image.png
extensionRegistry 由 ExtensionRegistry 类实例化产生。
image.png

extensionRegistry 如何收集拓展配置

这里以 CommandsRegistry 为例,先剧透一下,CommandsRegistry 具体内容后续介绍。可以看到,它通过调用 extensionRegister.addCoreModule 这个方法,将 CommandRegister 配置进行收集。
image.png

extensionRegistry 如何注入拓展配置

可以找到 initApp, 这个函数,他会在项目启动的时候掉用,将 extensionRegister 作为参数传入,在函数体中,配置依赖被加载到 container 中,然后注入到 App 实例上,成为它的属性,最后 App 实例被返回

这个container 是 mana- syringe 提供的,就是之前提到的 IOC-container

image.png
在项目入口目录中也可以发现,我们之前提到的 XFlow 实例 App,就是 initApp 的返回值,它会作为上层状态值,保存在 XFlowAppInternalProvider,供子组件调用。
image.png

XFlowCanvas

image.png
XFlowCanvas 是 XFlow 最核心的画布组件, 它封装了 X6 提供的画布, 提供默认画布配置项、透传X6支持的所有事件并提供类型推导, 同时也允许用户自定义需要渲染的React节点和连线上需要渲染的React内容。

DOM 结构

可以看到 XFlowCanvas 的 DOM 结构很简单,就是最外层两个 div,我们可以称为父容器和根容器,并通过 ref,提供了访问它们的方法。
image.png

IOC-container 与依赖注入

XFlowCanvas 的相关配置也会被收集 IOC-container 中,然后被注入的 App 实例上。
image.png

GraphConfig

这个定义了与画布有关的配置,里面保存了画布的唯一 id、容器节点、画布的配置,提供了获取画布配置的方法,同时,像 setNodeRendersetEdgeRender这些我们常用的 API 也都在这里定义。
image.png

实例化 X6Graph

也许有朋友会问,这里搞来搞去,看起来都是 XFlow 在自嗨,那么作为 X6 的上层,它是如何与 X6 的 graph 联系上的呢?
其实 XFlow 的 graph 与 X6 的 graph 真正沟通的桥梁是 GraphManager这个类,它提供了 getGraph 的方法,获取 GraphConfig 中的配置,并通过 const graph = newX6Graph({}) 实例化,最后返回给用户。可见我们上面讲到的 GraphConfig 更像个中介,它只是在 XFlow 这层暂存相关配置,而最后都是要传给 X6 层的 Graph,产生真正的画布。
image.png
我们前面提到的 setNodeRendersetEdgeRender 也是如此,GraphConfig 类做的事只是把自定义节点/边存到 nodeRenderedgeRender中(它们的本质是 ES6 的 Map),最后真正的注册工作仍然是要交给 GraphManager
image.png
image.png

XFlow 中获取画布实例与配置

在 XFlow 中可以通过 getGraphInstancegetGraphOptions获取画布实例与配置,getGraphInstancegetGraphOptionsGraphProvider 的方法,而 GraphProvider 会借助 mana-syringe 注入到 App 实例 上 。

useDynamic 是 mana-syringe 的 API, 可以基于带有容器信息的上下文给出实例

image.png

ModelService

在 XFlow 通过 Model 来驱动 React UI 组件更新渲染,我们通过监听事件可以在事件的回调函数中调用 Model 的 setValue 方法 来更新 Model,UI 组件中通过 Model 的 watch 方法来更新组件内部的 State,从而实现组件渲染的更新。
image.png

ModelRegister

ModelService 中最为核心的部分,他会被收集到 IoC-contaier,并注入到 App 实例上,成为 App 的实例ModelService。就像源码注释中说的那样:“注册 ModelRegistry alias IModelService”。所以,在狭义上,ModelRegister 就是 ModelService!
image.png
image.png
讲了这么多,在回到源码,看看它的真面貌吧。这块内容有点多,截图截不下了,大家自行去看项目里看吧。
image.png
可以看到 ModelRegister 是一个类,由接口 IModelService 约束。而 IModelService 定义了三个方法:

  • registerModel: 用于注册模型,所有被注册的模型会被存到一个 Map 中。
  • awaitModel: 异步获取模型,也就是拿着用户传入的 token 去 Map 中查找。
  • findDeferredModel: 用于同步获取模型

说白了,其实就是提供增删(没有删)改查的功能。
image.png

Model 的实质

说了这么久的 Model(模型),那他到底是个啥呢?那自然得从注册模型的方法 registerModel 里面找答案了。可以看到 Model 实质上是 RxModel 的实例,至于下面的 watchChange,是用户传入的回调函数,我们在注册的时候调用它,主要作用是为了绑定一些事件监听,从而更新 Model 的值。
image.png
所以我们再去相关目录下看看 RxModel(这种沙里淘金的感觉真是太刺激(难受)了)。RxModel 的核心是 subject$ 属性,它是 RxJS 提供的 BehaviorSubjectRxModel 中保存了一个 value,并暴露了一些方法用于对 value的 CRUD。

  • watch 传入一个回调函数来订阅 model 的变化。这里利用 RxJS 提供的订阅能力。
  • setValue 更新 model: 支持传值或者传入更新函数。
  • getValue 获取 model 的值

image.png

内置 model

XFlow 中内置了一些 model,可以直接使用,无需额外注册,我们称为 MODELS。可以看到,它们都被定义在constant.ts 的目录中,并通过 import * as MODELS from './constant'的方式统一暴露,所以,MODELS 其实就是类似一个平平无奇的 object
image.pngimage.png
大致可以理当为如下结构,不过本质上不太准确,因为它由是 ESM 的统一暴露所产生的,而属性值其实是 namespace

  1. const MODELS = {
  2. IS_NODE_SELECTED: {
  3. id: 'IS_NODE_SELECTED',
  4. getModel: getModelUtil<IState>(id),
  5. useValue: useModelValueUtil<IState>(id)
  6. },
  7. SELECTED_CELL: {
  8. id: 'IS_NODE_SELECTED',
  9. getModel: getModelUtil<IState>(id),
  10. useValue: useModelValueUtil<IState>(id)
  11. }...
  12. }

内置 model 的注册

当然,内置 model 也不是石头里蹦出来的,其实也是因为已经由 XFlow 提前注册好了,所以才能直接使用。这里以 MODELS.SELECTED_CELLS 为例,简单看一下。很容易理解,它的初始值是数组,在回调函数 watchChange中通过graph.on(selection:changed)监听画布变化,更新 model 的值。
image.png

CommandService

Command 是解耦 UI 组件和 Graph 的关键,所有对 Graph 的操作都通过 CommandService.executeCommand 来完成

那些年踩过的坑

先吐槽一个点,关于 command 的逻辑被放在了两个 不同的目录里面,导致我一开始没咋搞明白这块是怎么回事,当时光顾着看 command-contributions,没注意到 command,就很…懵逼…
image.png

CommandRegister

它的地位就像 ModelRegister,也是最核心的存在,当我们通过 const { commandService } = app,从App 实例上获取 commandService 时,拿到的其实就是CommandRegister 的实例。
CommandRegister 的属性和方法比较多,我们重点就看一下 excuteCommand 方法。像我们执行 CommandService.executeCommand 的时候,其实就是调用了这个方法,在方法中通过 cmd.execute() 执行命令。
image.png

内置命令

在 XFlow 中内置了非常多的命令,总体上可分为 edge、graph、group、models、node 五类(忽略下图的components,这是另外的玩意)。
image.png
下面我们以 node-back 为例进行简单说明,看一下这些命令的实现细节,可以看到,它实际上就是调用 x6Graph?.getCellById(nodeId)拿到选中的节点,再通过 x6Node.back()完善节点后置。这两个都是 X6 的 AP。而 argshandlerArgs 就是我们用户传入的参数

这里的 BackNodeCommand 就是上面的 excuteCommand 中的 cmd,其实具体的细节就有大家自行去看吧,用到了很多 mana-syringe 的 API,偷个懒就不写了。

image.png

hooks

看了上面 BackNodeCommand 内置命令的代码,可能有朋友会问,为啥有个 hooks.backNode.call()。它是通过 this.ctx.gethooks() 获取到的,this.ctx采用了依赖注入,可以理解为一个辅助性的上下文属性,通过它可以获取 hooks、画布实例、画布配置等。这里不做过多赘述。
我们直接去看 hooks !所有 hooks 其实都是 HookHub 的实例,在 XFlow 的目录结构中,它被列为一个单独的子包。
image.png
hooks.backNode.call 其实就是调用了下面的 call 方法。
image.png
而第二个参数 main,其实就对应了下面红框中的回调函数
image.png
可以看到,在 call方法内,实际上是调用了scheduler(args, main, hooks)。而 scheduler 是从 this.scheduler 中取得的。如下图所示,this.scheduler其实就是一个对象,它有两个属性,属性值都是函数。这两个函数定义了与命令相关的 hooks 不同的执行策略,分别是 pipeline 串行执行async 并行执行。默认情况下采用 pipeline 串行执行。

这些 hooks 由用户注册,可以理解为对命令参数和命令回调本身做一些处理,比如取消命令的执行等

image.png
下面具体看一下 scheduler 函数的代码,其实就是先按照顺序执行所有 hooks,最后再执行回调。
image.png
绕一大圈,总结一下,当我们调用 commandService.executeCommand(),其实执行的是 hooks.backNode.call(),它所做的事就是先执行所有与后置节点命令相关的 hooks,然后再执行命令。

源码阅读心得

  • 全局意识。拿到 XFlow 的源码不用着急直接看,可以先从全局入手,看一下目录结构和官方文档,理解它的主要构成部分
  • 抓住主干。阅读一段代码时,要抓住主干,先知道它主要是做什么的,对于一些枝叶部分可以先选择跳过。例如下面这段代码 addNode 命令的代码,其实主要工作就是调用 X6 的 graph.addNode(nodeConfig, eventOptions),向画布中添加节点,至于 createNodeServicethis.processNodeConfig()之类的,其实都是一些辅助性的手段,进行一些前置处理,并不是核心部分,可以先选择性忽略,大概看一下

image.png

  • 关注与 X6 的衔接。XFlow 本质上对 X6 的封装,在看代码的过程中,我们要关注它是如何调用 X6 的 API ,找到 XFlow 与 X6 连接的桥梁。
  • 细看注释,语意猜测。XFlow 中有很多注释,在阅读的过程中可以仔细看。这年头变量起名都是语意化的,很多函数看名字就知道它的大概作用了。
  • 结合示例。单看源码,其实挺抽象的,我们可以结合官网的 demo 和示例代码去看。