从2022年3月份开始,到现在开发 AntV G2 5.0(蚂蚁集团开源的企业级可视化框架) 超过两年了,我在这个过程中学到了什么?让我们一起来看看。
案例是最有效的塑造工具的手段
G2 本质上是一个工具,一个做可视化的工具,它的语法的设计和推广主要通过案例来实现的。在设计了初版语法之后,我花了不少的时间来做案例验证,确保现有语法可以覆盖所有预期的示例。如果不能,就对语法进行调整,直到能描述所有的案例都。当然这个过程存在于整个开发过程中,因为不断会出现之前没有考虑到的案例。而对于用户来说,案例永远是他们最先看的东西,而不是文档的好坏,因为案例是最直接能展示工具能力,并且方便用户复制粘贴。大部分用户没有足够的耐心和时间去仔细学习工具的 API,他们更看重是否有满足需求的案例。此外,高质量的案例也是吸引流量、提升影响力的有效方式,比如“亚运可视化”让 G2 登上了云栖大会的舞台,“汪峰情感线可视化”给 AntV 公众号带来了 70W+ 的阅读数。
10% 的代码里面藏着 90% 的 Bug
听上去有点不可思议,但事实确实是这样。在 G2 中,有一些代码在我第一次提交之后就几乎没有再改过,比如 Bin,Group 这些数据转换模块。然而,有的代码从发布到现在都一直有人在提 Bug 并进行修复,比如 Tooltip,Slider 等交互模块。这并不意味着后者就一定并前者复杂,只是有可能后者涉及更多的条件分支和状态。对于前者有不低的开发成本,但是对于后者有不低的维护成本。
OOP 用于组织 API,FP 用于底层实现
主管在我设计 G2 5.0 的架构时问我,什么是好的架构?当时我回答的不好,我现在也不知道确切的答案。不过我个人不太喜欢复杂或者抽象程度高的架构,尤其是某些设计模式。这些设计模式通常基于面向对象编程(OOP)的范式,其最显著的特征之一便是“不透明性”:它会封装一些参数成为内部状态,也会在方法内改变这些状态,让调用者这不感知。这对于上层用户是很友好的,因为这避免了显示地管理重复的、不重要的参数,特别是多个函数都需要同样的参数时,这降低了使用的复杂性。但是对于内部实现却不太友好(特别是加上继承的情况),它使得内部状态不容易追踪。面对这类代码,我常会问自己,“这个变量哪来的?”,“这个方法到底依赖哪些变量?”,“这个方法又修改了哪些变量?” 反观函数式编程(FP),尤其是对于纯函数(没有副作用的函数),则刚好相反:每一个函数需要声明完整的参数列表和返回值。虽然这可能使得参数列表较长,但是数据流会变得更容易追踪,提高了代码的可预测性。基于此原则,G2 几乎所有的 API 都是通过 Chart 对象暴露出的,底层实现就是一个大的 render(options, context)
函数,该函数负责把指定的Spec转换成最终图形并渲染。这个大的 render 函数本身又是由一系列尽量“纯”的子函数构成。这些子函数按照功能的分类,共同形成了 Runtime + Lib 的架构。基于 FP 的实现,也是得代码容易重构、拆分,对快速的持续集成和包大小很友好。毕竟有一句对 OOP 经典的描述:“我想要一根香蕉,却给了我一只拿着香蕉的猴子,以及......背后的森林。”
没有完美的 API 设计,只有最合适的取舍
在最初设计 API 阶段,我常与团队成员展开争论一些很细节的一些点,大家都渴望建立一个完美的 API。但随着时间推进,我发现争论对原因来自于:我更在于 API 的专业性,而团队成员则更看重其兼容性。所以,更好的做法是根据目标提出一些容易度量的标准,然后基于这个标准去对设计进行取舍。
过早的语法糖只会带来额外的维护成本
在 G2 开发的早期,为了追求语法的简洁,我们设计了若干语法糖。这意味着更多的代码、Bug 和 文档,但却没有带来新的特性。语法糖会把降低的用户的使用成本变成开发者的维护成本,这笔交易在基础能力稳定之后添加会更加合算:先确保系统的健壮性和可靠性,随后再通过引入语法糖来提升用户体验。
好的测试让重构变得简单
测试在什么时候存在感最强?不是在新增特性的时候,而是在重构代码的时候。G2 在早期没有集成测试的情况下,每次为了改善扩展性而重构内部架构后,都要求我们在 jest-electron 环境里面把所有测试审查一次。如果发现问题并且修复之后,又得重新审查一次,这个过程效率是很低的。但自从有了集成测试之后,可以自动化的发现重构引入的问题,这大大增加了测试环境的稳定性和可靠性,从而显著提高了迭代的速度。夸张的讲,如果 G2 完全重构实现一个 6.0 的版本,只要视觉风格没有变,现在的测试案例仍然可用。这里必要要感谢沧东,最开始我提出截图测试想法的时候,不知道如如果落地,是他开发出了第一个版本,也是他最后解决了跨平台的问题。
修复了一个 Bug 一定要添加对应的测试案例
这应该算是主管教我的第一课,也是我试用期印象最深的一点。其原因也很简单:这样才能确保相同的 Bug 不会重复出现,让 Bug 数量趋于稳定。因此,当我遇见了修复了 Bug 单没有提供测试案例的 PR,我都会要求修改。这其实也类似我们犯错后不能只总结错误原因,还要制定未来的预防措施。
挑战者除了要有能力,更重要的是耐心,以及机遇
G2 5.0 是一个挑战者,是 ECharts、Vega 和 Plot 这些优秀前辈的挑战者,更是 G2 4.0 的挑战者。最开始我以为,只要 5.0 的能力比它们强,就一定能挑战成功。但后来发现,能力强其实只是其中的一环,甚至不是最重要的一环。除了光鲜亮丽的事情,还有很多琐碎去做:修复 Bug、能力稳定、处理 Issue 和运营社区等。在能力没有显著差异的时候,这些可能更能打动你的用户,但这些是需要耐心的。在这个过程中,我渐渐意识到:所谓耐心不是花大量时间去做你擅长和热衷的事情,这是本心。真正的耐心,是花大量时间去处理你不擅长或者没有那么感兴趣的事情。对我来说,写代码是最简单的,但是写规划、运营社区等就会相对复杂。当然,不是所有的花都会开,有时还得听天命。
区别公司开源项目和个人开源项目很重要
G2 5.0 正式版本的发布曾经被推迟,当时我一直找不到根本原因。直到主管给我说:“这不是你的个人项目,你得为团队成员负责。” 这才使我意识到把太多个人的倾向和偏好引入团队项目不一定是好事,比如过严的 CR、优先添加自己感兴趣的特性等等。混淆这两者,会让你缺失对事情优先级的把控,从而导致项目的落后。从另一方面来讲,区别这两者,有助于定目标,因为公司开源项目和个人开源项目的目标是不同的。除了影响力这种比较“虚”的目标之外,一个较实的目标是:吸引外部开发者共建,减少公司的投入成本。这一点也是后来 AntV OSCP 希望达到的目标之一。
面向特性编程不如面向用户编程
G2 5.0 中会觉得一些的特性我觉得很酷,但其实用的人很少,比如可视化叙事和单元可视化等。这种情况或多或少会引起一定的落差感,并且怀疑所有工作的价值。然后,与此相反,我发现帮助用户解决 issue 中的问题,很能获得成就。因此,今年下半年,G2 改变了迭代的策略:以用户为导向,去添加用户在 issue 中提出的真正期望和需要的功能,并且解决他们在使用过程中遇到的问题。
维护开源项目必须学会处理负面情绪
在 Github 上你常常会遇见一些你不愿意面对的用户:提 Bug 就一句话;什么都要问,就差你帮他把代码写完;直接把需求给你,让你帮他过一遍看行不行...... 并且这些用户的态度往往不讨喜,觉得你回答他的问题是天经地义的事情,最经典的观点就是:“你做开源,公司没有给你钱吗?” 最开始我遇见这样的人,我是很炸裂的,很想怼回去,比如:“给我钱的是公司又不是你”,“ECharts 那么好用,那你别用 G2 啊”。但后来想想,怼回去没有任何意义,不能解决任何问题。你应该从他的情绪和你的情绪中跳出来,看看他在意的真正问题是什么,后面的回复都集中在这个问题上,不要被情绪带偏了。这道理在日常交流中同样适用,要时刻问:问题的本质到底是什么?我现在的行为是否有助于解决问题?
先相信,再质疑,最后把荣誉给他人
在开始 AntV OSCP 计划的时候,因为考虑到外部开发者的水平有差异的,我常常在纠结这个问题是不是太难了,所以最开始发布的任务少且简单。但后来我发现,发布任务的时候,应该把外部开发者都当成和你一样或者超过你的水平。你真正需要做的是高质量的 CR,甚至帮开发者改代码,并且确保是以开发者的名义合并代码的。这样才能让更多的人参与到开源项目的共建当中。当然,事实也如此,当我按照这个思路运营 AntV OSCP 计划之后,G2 的月开发者从最初的3到5人,到了20+,并且解决了一些核心开发者没有解决的问题。同时参与的开发者也很开心,甚至出现了持续贡献的开发者。
不要让别人去猜
在日常沟通中有一个常见的场景:有人直接发给你一张图片或者一篇文章,并且不带任何说明或者上下文,让接收者不得不去猜测对方的真正意图。这种沟通方式不仅效率低下,还可能会误解读,因为大家没有足够时间去猜。这也提 Bug 的时候一直强调提供复现 Demo 的原因,只有有足够多的上下文信息,才能快速定位问题,而不是靠一点点猜到问题。有人可能会觉得为啥我的问题都这么明显了,还是没有人帮助我?真实情况是,别人可能没有那么关注你,或者没有觉得你的问题是问题。与其内耗,不如清楚说出自己的问题,寻求帮助,这没什么丢脸的。
在合适的环境,和合适的人,做合适的事
能在公司几乎不受影响的做开源,这一点让我一直觉得很幸运,也是我一直很感谢我团队和主管的一点。找到一个有归属感的环境,有理解和支持你的人,是一件不容易的事情。
今天的工具塑造明天的艺术
尽管之前提到“面向特性编程不如面向用户编程”,但是我认为我们作为工具制造者有一个重要的使命:让用户看到更多的可能性。我们有责任和义务去给用户带来先进的能力,或者把已有的能力用全新的方式透出,给用户更多的创作空间。当有人 G2 的 Transform API 去分析数据的时候,有人用 G2 的嵌套分面能力实现单元可视化的时候,我都觉得我达到了这一点。所以我对 G2 的期待一直都不只是一个可视化框架,而是一个可视化语言,能传递背后可视化思维的语言。我一直坚信,今天的工具能塑造明天的艺术。
最后,我把以上在开发 G2 学到的东西都应用到了一个新的个人项目:“Charming: The declarative API for 2D graphics.” 说人话就是一个高性能的声明式渲染框架,下面是一些案例,可以在 Observable 上看更多的例子。如果觉得不错的话,欢迎大家去 Github 上点一个小🌟🌟!
最后的最后,Today's tool help shape tomorrow’s art!