图表即代码是将图表以领域特定语言作为载体,围绕于不同的使用场景,转译生成二次产物 —— 如概念图、架构图、软件架构等。
对于造图形库这个库,我的想法由来已久。然而,直到最近,积压的需求越来越多的时候:
-
随着,我们在 ArchGuard 中的架构工作台的进一步深入,需要构建一个架构设计线上化的功能。对于 ArchGuard 平台而言,设计线上化并意味着在线设计架构。在初期,我们想提供的是:架构图的线上化呈现,也就是可以通过代码化架构图的方式,诸如于 Mermaid 就可以提供这样的功能。
-
与此同时,在半年前,Quake 框架 也卡在这样一个可视化的图形库中。如何可视化知识的连接?建立它们的连接?
-
在那上一篇文章《思维图形化:从表象到概念的浮现过程》中,也需要这样的一个工具,作为它们的载体。然而,现有的工具,在版本化这事上做得可毒啊了。
于是,在挖坑之前,我开始思索我要构建的是怎样一个图形库。值得庆幸的是:哪怕不存在上述的三个原因,我也打算造一个轮子。当然,之前的重点可能不是可用,现在必须要提供一个可用的轮子。
图表即代码
作为一个《万物即代码》(https://ascode.ink/)的先行者,对于图表来说,它的可版本化管理依旧是一个痛点。所以,在这个新的工具实现图表即代码,依旧是一个非常有意思的探索点。
图表即代码(Diagram as Code)是一个已经有一定基础的领域,在我与我的同事们一起构建开源应用 Ledge 的时候,我们已经大量地采用了这个思想。稍有不同的是,Ledge 的图表即代码偏向于是数据可视化,而我们即将要构建 Feakin 偏向于是概念/想法的可视化。但是,这并不影响,我们再次定义一下图表即代码。在先前的《文档代码化》 中,我们定义的文档代码化是:
文档代码化,将文档以类代码的领域特定语言的方式编写,并借鉴软件开发的方式(如源码管理、部署)进行管理。它可以借助于特定的工具进行编辑、预览、查看,又或者是通过专属的系统部署到服务器上。面向非技术人员的文档代码化的一种常见架构模式是:编辑-发布-开发分离』,
而对于图表即代码来说,它是可以相似的方式来定义的:
图表即代码是将图表以领域特定语言作为载体,围绕于不同的使用场景,转译生成二次产物 —— 如概念图、架构图、软件架构等。
这个定义从某种意义上是围绕于现有的工具而产生的,诸如于:
-
可视化架构图。在 Coca 中,我们使用 Graphviz 来生成软件的依赖关系;在 GitHub 网页上,可以使用 Mermaid 来编写 README.md。
-
生成代码。诸如于 PlantUML,利用工具可以从 UML 到代码骨架生成;如 Structurizr DSL,可以让从 C4 模型生成 PlantUML 图,进而生成代码。
-
交互的图表。如在 Ledge 中,生成的图形本身是可以调整和交互的。
对于这样的系统,我想大家都知道如何去设计了。或者说,至少在心底是有个印象。
领域特定语言描述
作为代码化的第一要素,它必须是采用 DSL (领域特定语言) 的设计,才能有效地进行代码化。值得注意的是不要畏惧 DSL:采用领域特定语言,并不意味着特别复杂的编译实现,哪怕是 JSON 格式描述,也可以适为一种 DSL。所以,诸如于 Graphviz 中设计的 DOT Language 就非常的简单:
-
digraph G {
-
a -> b
-
b -> c
-
}
简单的语法,可以生成非常有用的图形。只在我们需要一些额外的配置时,才需要去翻看对应的文档。而如果我们能提供更多的 samples,那么就能降低查看文档的成本,构建更好的开发体验。
从另外一个层面来说,图形的序列化结果,其实也算得是上一种领域特定语言。诸如于 .excalidraw
的 JSON 形式, .drawio
文件采用的编码后的 mxgraph 的 XML 格式,它们都是图形的一种类型的领域特定语言。
布局计算:算法生成的关系图
对于代码生成图形来说,用过 D3.js 或者是 Echart.js 的小伙伴,对于 Dagre、ForceLayout 等一系列的图形自动布局算法不陌生。在这里就不展开了 —— 主要是我也不是算法专家。
随后,布局的计算依赖于数据 + 模型,对于一个图表既代码的系统来说:
-
模型,依赖于 DSL 生成的构建的模型。其中大部分是隐式的模型,如上述 DOT 语言中的
a
和b
是节点,而→
是指向关系。 -
数据,来源于 DSL 又或者是数据源。如 Graphviz 中来源于 DSL 中的代码,而在支持 import 关系的 DSL 中,则可以通过 DSL 来导入数据。
当然了,如果能提供一个抽象的算法接口,以接入更多的布局算法,那么就可以大大提高系统的灵活性。在这一点上 Cytoscape.js 就做得挺好的,提供了 ELK、CoSE、Cola、fCoSE 等算法的接入,底层的灵活性会带来更多的可扩展空间。
二次转译:支持后续活动
从现实的因素来考虑,并非所有的图表都应该用图表即代码的方式。人们采用图表即代码这种方式,也意味着:基于可视化的结果,进行后续的活动。诸如于:
-
采用 Structurizr DSL、PlantUML 来呈现系统的设计,会考虑用它来生成模板代码,并在后续对比实现架构与目标架构的差异。
-
采用 Graphviz 来生成系统依赖关系,用它来展示系统中的循环依赖,再通过自动化地方式检测。
-
……
也因此,与其说是图形即代码,不如说图形化只是中间的产物,作为沟通时的信息载体。在这点上,它与设计即代码颇为相似,DSL 充当的是图形的标准化输出。
可选的双向绑定:代码 < - > 图形
与上述的内容相比,在代码与图形之间提供双向绑定显得非常有意思。代码化可以向程序员提供高效的输入方式,但是正如新手程序不习惯用 Terminal 一样,他们也需要图形化的方式。于是呢,如何在改变图形的同时,更新代码就变得非常有意思了。从结果上来说,图表工具在保存的时候,存储的是数据模型,而模型便是这个双向绑定的基础。如在使用 draw.io 这样的可视化工具时,当我们添加新的矩形、连接时,结果会更新到对应的数据模型中。
而图形化的编辑呢,存在一些额外的动作(action),如我们在撤销(undo)、重做(redo)的时候,要提供这种模型的重载。这些因素显然会带来一些额外的工作量。
Feakin:面向概念构造系统
于是乎,为了在 ArchGuard 和 Quake 中采用,我便在构思如何去设计这样一个图形工具,名为 Feakin —— 为了注册到 GitHub 的组织里:https://github.com/feakin/ 。随后,为了和 fxxk 做一个区分,Logo 是:f(k)
围绕于如何通过概念来构建系统 —— 即我们如何通过图形来传送想法,是 Feakin 的核心考虑因素。也因此如何支持层次化的思维表达,是 Feakin 的一个重点,也因此,诸如于 Echart.js、Ant G6 等面向数据的图形引擎,并不是 Feakin 的同类型产品。Feakin 的同类型工具是:Drawio、Excalidraw、Graphviz、Mermaid 这一类程序员工具箱里的工具。
Todo —— 图表的抽象模型
起初,在设计 Feakin 的时候,我只想构建一个 Draw.io 的兼容引擎。于是,构建了一个 PoC:https://feakin.github.io/drawio-render-poc/ 。只是呢:
-
Draw.io 的代码是 ES5 时代的核心引擎
-
mxgraph 已经不维护了,一个难以维护的遗留系统
Draw.io 的其中一个可参考的点是 —— 内置了对其它图表库的支持,如 Mermaid、OrgChart 等等。
所以,如何构建一个图表的抽象模型,并提供它们的转换就非常有必要。
Todo —— 图表引擎:Feakin Render
随后,在有了模型之后,我们就需要 Render。当前在 PoC 版本里,我们用的是同事建议的:react-konvas,一个基于 Canvas 的工具库。从功能上来说,它似乎能满足当前的需求。
Demo 地址:https://github.com/feakin/diagram-render
不过呢,我正在慢慢学习不同图表工具的设计,所以进展不是很理想。现在只支持基本的渲染 + 布局接口。
Todo —— 面向扩展编程
当然,还只是想想。
Feakin 总结
啊,说实话,其实就是只有一个想法 + PoC。