近年来,阿里数据中台产品发展迅速。核心产品之 Quick BI 连续 2 年成为国内唯一入选 Gartner 魔力象限的国产 BI。Quick BI 单一代码仓库源码突破了 100万行。整个开发过程涉及到的人员和模块都很多,因为下面讲的一些原则,产品能一直保持在快速的开发状态。
先分享几个关键数据:
  • 代码:TypeScript 82万行,样式 Sass+Less+CSS 18万行。(cloc 统计,去除自动生成代码)
  • 协同:Code Review 12,111 次,Commit 53,026 次。
很多人会问,这么多代码,为什么不切分代码库?还不赶快引入微前端、Serverless 框架?你们就不担心无法维护,启动龟速吗?
实际情况是,从第一天开始,就预估到会有这么大的代码量。启动时间也从最初的几秒钟到后面越来越慢5~10分钟,再优化到近期的5秒钟。整个过程下来,团队更感受到 Monorepo(单一代码仓库)的优势。
这个实践想说明:
  • 大的 Codebase 可能是好事情,大道至简。用极其“简单”的架构更容易支持复杂灵活的业务
  • 要做到简单的架构,内部需要更明确的规范,更密切的协同,更高效的执行
  • 能通过工程化解决的问题,就不要通过开发规范,能通过规范来解决的不要靠自由发挥
开工
2019年4月30号,晴朗的下午,刚好是喜迎五一的前一天,发挥集体智慧,投票选出满意的仓库名。同时借 Quick BI 和 FBI 底座融合的契机,项目开启。后来底座代码转正,把上层业务代码也吸纳进来。
commit769bf68c1740631b39dca6931a19a5e1692be48dDate: Tue Apr 3017:48:522019 +0800 A New Era of BI Begins
Why Monorepo?
在开工之前,对单一仓库(Monorepo)和多仓库(Polyrepo)团队内做了很多的讨论。
曾经我也很喜欢 Polyrepo,为每个组件建立独立 repo 独立 npm,比如2019年前,单是表单类的编辑器组件就有 43 个:
本以为这样可以做到 完美的解耦、极致的复用??
但实际上:
  1. 每次 Babel、React 等依赖整体升级能让人脱层皮,所以自研了脚手架。造轮子都是被逼出来的,事情做了一点点,但写脚本能力直线上升
  2. 每次 调试组件,npm link 一下。后来组件跨级,可以做 3 层 npm link,使用过的都知道这是多么糟糕的体验
  1. 版本难对齐,每次主仓库发布前,组件间版本对齐更是考验眼力,稍有不慎触发线上故障
  2. 方便别人复用的优势呢?最终支持自己业务都捉襟见肘,哪还敢让别人复用
最终我们把所有这些组件都合并到一个仓库,其实像 Google/Facebook/Microsoft 这些公司内部都很推崇 Monorepo。
但我们不是原教旨主义的 Monorepo,没必要把不相关的产品代码硬放到一起。在实线团队内部,单个产品可以使用 Monorepo,会极大降低协同成本。但开始的时候,团队内还是有很多疑问。
关于 Monorepo 的几个核心疑问?

1. 单一仓库,体积会很大吧?

100 万行代码的体积有多大?
先来猜一下:1GB?10GB?还是更多?
首先,按照公式计算一下:
代码的体积 = 源码的体积 + .git 的体积 + 资源文件(音视频、图片、其他文件)
  1. 我们一起来算一下源码的体积:
一般建议每行小于 120 字符,我们取每行 100 个字符来算,100 万行就是:
100* 1000,000 = 100,000,000 B转换之后就是100 MB!
那我们的仓库实际多大呢?
只有 85 MB!也就是平均每行 85 个字符。
  1. 再来算一下 .git的体积:
.git里记录了所有代码的提交历史、branch 和 tag 信息。会很大体积吧?
实际上 Git 底层做了很多的优化:1. 所有 branch 和 tag 都是引用;2. 对变更是增量存储;3. 变更对象存储的时候会使用 zlib 压缩。(对于重复出现的样板代码只会存储一次,对于规范化的代码压缩比例极高)。
按照我们的经验,.git记录 10,000 次 commit 提交只需要额外的 1~3 个代码体积即可。
  1. 资源文件大小
Git 做了很多针对源码的优化,但视频和音频这类资源文件除外。我们最近使用 BFG 把另一个产品的仓库从 22GB 优化到 200MB,降低 99%!而且优化后代码的提交历史和分支都得到了保留(因为 BFG 会编辑 Git 提交记录,部分 commit id 会变化)。
以前 22 GB 是因为仓库里存放视频、发布的 build 文件和 sourcemap 文件,这些都不应该放到源码仓库。
小结一下,百万行代码体积一般在 200MB ~ 400MB 之间。那来估算下 1000 万行代码占用体积是多少?
乘以十也就是 2GB ~ 4GB 之间。这对比 node_modules随随便便几个 G 来说,并不算什么,很容易管理。补充个案例,Linux 内核有 2800 万行,使用 Monorepo,数千人协同。据说当时 Linus 就是为了管理 Linux 的源码而开发出 Git。

2. 启动很慢吧?5分钟还是10分钟?

听到有些团队讲,代码十几万行,启动 10+分钟,典型的“巨石”项目,已经很难维护了。赶紧拆包、或者改微前端。可能团队才 3 个人却拆了 5 个项目,协同起来非常麻烦。
我们做法有3个:
  1. 按照页面来拆分多 Entry,每次只需启动一个 Entry
  2. 梳理子包间的依赖关系,追求极致的 Lazy loading,Tree-Shaking
  1. Webpack 切换到 Vite
尤其是 Webpack 切换到 Vite 以后,最终项目冷启动时间由 2-5分钟 优化到 5秒 内。热编译时间由原来 5秒 优化到 1秒 内,Apple M1 电脑基本都是 500ms 以内。

3. 代码复用怎么办?Monorepo 复用的时候是否要引入全部?

传统的软件工程思想追求 DRY,但并不是越 DRY 越好。
每写一行代码,都产生了相应代价:维护的成本。为了减少代码,我们有了可复用的模块。但是代码复用有一个问题:当你以后想要修改的时候它就会成为一个障碍。
对于像 Quick BI 这样长期迭代的产品,绝大部分需求都是对原有功能的扩展,所以写出易维护的代码最重要。因此,团队不鼓励使用 magic 的特技写法;不单纯追求代码复用率,而是追求更易于修改;鼓励在未来模块下线的时候易于删除的编码方式。
对于确实存在复用的场景,我们做了拆包。Monorepo 内部我们拆了多个 package(后面有截图),比如其他产品需要 BI 搭建,可以复用 @alife/bi-designer,并借助于 Tree-Shaking 做到依赖引入的最小化。
目前的开发体验
  1. 冷启动 5秒,热编译 1秒内。以前是 5~10分钟。
  2. 改一行代码能解决的问题,真正改一行且发布一次。而不是改 10+ 个项目,按依赖发布 N 次。
  1. 新人 10分钟 搭建好环境,上手开发
    1. 相比于以前每个组件一个 Repo,包赋权都要搞很久
  1. 避免了版本不对齐的问题
    1. 对于 2C 产品,不需要多版本多主干分支,但多个 npm 依赖对齐版本也不容易
    2. 对于 2B 产品,由于多环境、多版本,会更加复杂,复杂度极高。Monorepo 通过分支来统一内部依赖的版本
  1. 工程化升级只需要一次。目前是基于 Lerna 开发的 Pri Monorepo 方案。
这样的体验要保持并不容易,开发中还有很多问题要解决。
真正需要解决的问题
并不是把代码放到一起就完了,背后复杂的问题是 协同、技术方案、稳定性(如何避免一个人提交代码导致整个产品崩溃?)

1. 包依赖管理

内部拆分多个子包,每个子包是子文件,可以单独发布 npm,见下图:
内部包管理的核心原则是:
  • 从左向右单向依赖,只能右边引用左边。避免循环依赖
  • 规范还不够,开发插件来自动检测,如果左边依赖右边直接报错
对于开源 npm 的引入,应该更慎重。大部分 npm 的维护时长不超过x年,即使像 Moment.js 这样曾经标配的工具库也会终止维护。可能有 20% 的 npm 是没人维护。但未来如果你的线上用户遇到问题,你就需要靠自己啃源码,陷入被动。所以我们的原则是,引入开源 npm 要三人线下评审通过才行。

2. Code Review 文化

互相 Code Review 能帮助新人快速成长,同时也是打造团队技术文化的方式。
过去几年一直在团队内推行 100% CR,但这还不够。机械的执行很容易把 CR 流于形式,还要分场景来做。
Monorepo 有个风险是一旦有问题就可能是整体的问题。
目前我们的 Code Review 主要分为3个场景:
  1. 线上 MR Code Review【1对1】
  2. 主题式 Code Review【3-5个人】
  1. 大版本发布前集体 Code Review【All】
12,111 次 Code Review 的经验很多,主要是:
  1. 及时 Review,鼓励小颗粒度的 MR,不必等整个功能开发完成
  2. 代码是写给人看的,鼓励白话文一样的代码,而不是文言文
  1. 建立最佳实践(目录树结构、命名规范、数据流规范)。开发一个功能可以有 10 种方法,但团队需要选 1 种并推广
  2. 不鼓励炫技,为了未来可维护性。能用简单技术实现,不要用“高深”冷门的技术
  1. 强调开发洁癖,追求优雅代码的文化。(命名是否易于理解、注释是否完整、是否有性能隐患等)

3. 工程化建设

这个过程首先要感谢淘系前端 DEF 工程化团队的支持,在这么多代码的情况下,不断挑战极限升级 DEF 支持我们。
除了制定文档的规范之外,能够自动化工具检查的规范才是好规范。

检查器:ESLint、TS 类型校验、Prettier

语法检查器是推动规范落地的重要方法,ESLint 可以做增量,优化后 git commit 的 pre-hooks 依旧很快。但 TS type check 因为不支持增量就比较慢了,需要搭配 CI/CD 来使用。

Webpack vs Vite

发布使用 Webpack,开发使用 Vite。
开发环境使用 Vite 快速调试,生产环境依旧使用 Webpack 打包。
风险是开发和生产编译产物不一致,这一块需要上线前回归测试避免。

4. 性能优化

对于数据产品而言,性能的挑战除了来自于 Monorepo 后资源包的变大,还有大数据量对渲染计算带来的挑战。
性能优化可以分为3个环节:
  • 资源加载:精细化 Tree Shaking,难在精细。Webpack 本身的 Tree-Shaking 做的并不好,不支持 Class method 做 Tree Shaking,所以有时候需要修改代码。Lazy Loading 模块做到按需加载,尤其是图表、SQL 编辑器这类大组件。合理的接口预加载,不要让网络闲下来。
  • 视图渲染:让组件渲染次数降到最低,表格类组件虚拟滚动优化,闲时预加载预渲染。
  • 取数请求:资源本地化缓冲方案,移动端使用 PWA 将 JS 等资源文件和数据缓存到本地。
另外还有性能检测工具,定位性能卡点。计划做代码性能门闩,代码提交前如果发现包体积增大发出提醒。

5. 数据化驱动架构优化

身在数据中台,我对数据的业务价值深信不疑。但对于开发本身而言,很少深度使用过数据。
所以 S1 重点探索了开发体验的数字化。通过采集大家的开发环境和启动耗时数据来做分析【不统计其他数据避免内卷】。发现很多有意思的事情,比如有个同学热编译 3~5 分钟,他以为别人也是这样慢,严重影响了开发效率,当从报表发现数据异常后十分钟帮他解决。
另外一个例子,为了保持线上打包产物的一致性,推动团队做 Node.js 版本统一,以前都是靠钉,钉多少次都无法知道效果如何。有了报表以后就一目了然。
目前整个数据化的流程跑通,初步尝到甜头。未来还有很多好玩的分析可以做。
更深层的经验

效率最高的方式就是一次最好

每行代码都会留下成本。长远考虑,效率最高的方法就是一次做好。
苏世民说“做大事和做小事的难度是一样的。两者都会消耗你的时间和精力”。既然如此,不妨把代码一次写好。代码中如果遗留 “TODO” 可能就永远 TO DO。客观来讲,一次做好比较难,首先是每个人认为的“好”标准不同,背后是个人的技术能力、体验的追求、业务的理解。

组织文化技术 相辅相成

技术架构和组织结构有很大关系,选择适合组织的技术架构更重要。
如果一个组织是分散的,使用 Monorepo 会有很大的协同成本。但组织如果是内聚的,Monorepo 能极大提效。
工程化和架构底座是团队的事情,靠个人很难去推动。
短期可以靠战役靠照搬,长期要形成文化才能持续迭代。
组织沟通成本高应该通过组织来解,通过技术来解的力量是渺小的。技术可以做的是充分发挥工具的优势,让变化快速发生。

简单不先于复杂,而是在复杂之后

对于一个简单的架构,总有人会想办法把它做复杂。踩了坑,下决心重构,成功则回归简单,失败就会被新的简单模式颠覆。踩坑本身也是有价值的,不然新人总是按捺不住还会再踩一次。做复杂很容易,但保持简单需要远见和克制。没有经历过过程的磨练,别人的解药对你可能是毒药。
架构不可能一成不变的,我们的图表最开始直接使用 D3、ECharts 很简单,后来定制很多逐渐复杂到难以维护,于是基于 G2 自研 bi-charts 后架构又一次变简单,前后的开发体验可能是差不多的,但背后的技术完全变了。
总结与展望
百万行代码没什么可怕,是一个正常的节点,仍然可以像几万行代码那样敏捷。
现在 Quick BI 已经向千万行迈进,向世界一流 BI 的目标迈进。以上内容更多是工程化相关,把工程化做好目的是想让开发者更专注于业务,没讲的业务挑战其实更多,因为数据分析天生就要与海量数据打交道,性能优化有长期的实践;洞察丰富异样的数据,有很多可视化及复杂表格方面的沉淀,可视化不仅是技术,也是业务本身;手机平板电视等多端展示,跨端适配的挑战。未来还希望能够把数据分析打造成一个引擎,能够快速集成到办公和商业流程中。
目前的开发模式并不完美,在迭代的过程中,不可避免会产生技术债,架构的优化本质就是在保持可维护性和减少技术债。最近团队在酝酿一次 Redux-Toolkit 的引入,会对取数和数据流有大的升级,有进展再分享。

关注「Alibaba F2E」微信公众号把握阿里巴巴前端新动向
继续阅读
阅读原文