前端100万行代码是怎样的体验?

本文涉及的产品
智能商业分析 Quick BI,专业版 50license 1个月
简介: 近年来,阿里数据中台产品发展迅速。核心产品之 Quick BI 连续 2 年成为国内唯一入选 Gartner 魔力象限的国产 BI。Quick BI 单一代码仓库源码突破了 100万行。整个开发过程涉及到的人员和模块都很多,因为下面讲的一些原则,产品能一直保持在快速的开发状态。
作者:会影
来源:Alibaba F2E公众号

image.png

近年来,阿里数据中台产品发展迅速。核心产品之 Quick BI 连续 2 年成为国内唯一入选 Gartner 魔力象限的国产 BI。Quick BI 单一代码仓库源码突破了 100万行。整个开发过程涉及到的人员和模块都很多,因为下面讲的一些原则,产品能一直保持在快速的开发状态。

先分享几个关键数据:

  • 代码:TypeScript 82万行,样式 Sass+Less+CSS 18万行。(cloc 统计,去除自动生成代码)
  • 协同:Code Review 12,111 次,Commit 53,026 次。

image.png

很多人会问,这么多代码,为什么不切分代码库?还不赶快引入微前端、Serverless 框架?你们就不担心无法维护,启动龟速吗?

实际情况是,从第一天开始,就预估到会有这么大的代码量。启动时间也从最初的几秒钟到后面越来越慢5~10分钟,再优化到近期的5秒钟。整个过程下来,团队更感受到 Monorepo(单一代码仓库)的优势。

这个实践想说明:

  • 大的 Codebase 可能是好事情,大道至简。用极其“简单”的架构更容易支持复杂灵活的业务
  • 要做到简单的架构,内部需要更明确的规范,更密切的协同,更高效的执行
  • 能通过工程化解决的问题,就不要通过开发规范,能通过规范来解决的不要靠自由发挥

开工

2019年4月30号,晴朗的下午,刚好是喜迎五一的前一天,发挥集体智慧,投票选出满意的仓库名。同时借 Quick BI 和 FBI 底座融合的契机,项目开启。后来底座代码转正,把上层业务代码也吸纳进来。

commit 769bf68c1740631b39dca6931a19a5e1692be48d
Date:   Tue Apr 30 17:48:52 2019 +0800

    A New Era of BI Begins

Why Monorepo?

image.png

在开工之前,对单一仓库(Monorepo)和多仓库(Polyrepo)团队内做了很多的讨论。

曾经我也很喜欢 Polyrepo,为每个组件建立独立 repo 独立 npm,比如2019年前,单是表单类的编辑器组件就有 43 个:

image.png

本以为这样可以做到 完美的解耦、极致的复用??

但实际上:

  1. 每次 Babel、React 等依赖整体升级能让人脱层皮,所以自研了脚手架。造轮子都是被逼出来的,事情做了一点点,但写脚本能力直线上升
  2. 每次 调试组件,npm link 一下。后来组件跨级,可以做 3 层 npm link,使用过的都知道这是多么糟糕的体验
  3. 版本难对齐,每次主仓库发布前,组件间版本对齐更是考验眼力,稍有不慎触发线上故障
  4. 方便别人复用的优势呢?最终支持自己业务都捉襟见肘,哪还敢让别人复用

最终我们把所有这些组件都合并到一个仓库,其实像 Google/Facebook/Microsoft 这些公司内部都很推崇 Monorepo。

但我们不是原教旨主义的 Monorepo,没必要把不相关的产品代码硬放到一起。在实线团队内部,单个产品可以使用 Monorepo,会极大降低协同成本。但开始的时候,团队内还是有很多疑问。

关于 Monorepo 的几个核心疑问?

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

100 万行 代码的体积有多大?

先来猜一下:1GB?10GB?还是更多?

首先,按照公式计算一下:

代码的体积 = 源码的体积 + .git 的体积 + 资源文件(音视频、图片、其他文件)

我们一起来算一下源码的体积:

一般建议每行小于 120 字符,我们取每行 100 个字符来算,100 万行就是:

100 * 1000,000 = 100,000,000 B
转换之后就是 100 MB!

那我们的仓库实际多大呢?

只有 85 MB!也就是平均每行 85 个字符。

2.再来算一下 .git 的体积:

.git 里记录了所有代码的提交历史、branch 和 tag 信息。会很大体积吧?

实际上 Git 底层做了很多的优化:1. 所有 branch 和 tag 都是引用;2. 对变更是增量存储;3. 变更对象存储的时候会使用 zlib 压缩。(对于重复出现的样板代码只会存储一次,对于规范化的代码压缩比例极高)。

按照我们的经验,.git 记录 10,000 次 commit 提交只需要额外的 1~3个代码体积即可。

3.资源文件大小

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
  3. 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分钟。

  1. 改一行代码能解决的问题,真正改一行且发布一次。而不是改 10+ 个项目,按依赖发布 N 次。
  2. 新人 10分钟 搭建好环境,上手开发

a.相比于以前每个组件一个 Repo,包赋权都要搞很久

4.避免了版本不对齐的问题
a.对于 2C 产品,不需要多版本多主干分支,但多个 npm 依赖对齐版本也不容易
b.对于 2B 产品,由于多环境、多版本,会更加复杂,复杂度极高。Monorepo 通过分支来统一内部依赖的版本

5.工程化升级只需要一次。目前是基于 Lerna 开发的 Pri Monorepo 方案。

这样的体验要保持并不容易,开发中还有很多问题要解决。

真正需要解决的问题

并不是把代码放到一起就完了,背后复杂的问题是 协同、技术方案、稳定性(如何避免一个人提交代码导致整个产品崩溃?)

1. 包依赖管理

内部拆分多个子包,每个子包是子文件,可以单独发布 npm,见下图:

image.png

内部包管理的核心原则是:

  • 从左向右单向依赖,只能右边引用左边。避免循环依赖
  • 规范还不够,开发插件来自动检测,如果左边依赖右边直接报错

对于开源 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个人】
  3. 大版本发布前集体 Code Review【All】

12,111 次 Code Review 的经验很多,主要是:

及时 Review,鼓励小颗粒度的 MR,不必等整个功能开发完成

代码是写给人看的,鼓励白话文一样的代码,而不是文言文

建立最佳实践(目录树结构、命名规范、数据流规范)。开发一个功能可以有 10 种方法,但团队需要选 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 版本统一,以前都是靠钉,钉多少次都无法知道效果如何。有了报表以后就一目了然。

image.png

更深层的经验

效率最高的方式就是一次最好
每行代码都会留下成本。长远考虑,效率最高的方法就是一次做好。

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

组织文化技术 相辅相成
技术架构和组织结构有很大关系,选择适合组织的技术架构更重要。

如果一个组织是分散的,使用 Monorepo 会有很大的协同成本。但组织如果是内聚的,Monorepo 能极大提效。

工程化和架构底座是团队的事情,靠个人很难去推动。

短期可以靠战役靠照搬,长期要形成文化才能持续迭代。

组织沟通成本高应该通过组织来解,通过技术来解的力量是渺小的。技术可以做的是充分发挥工具的优势,让变化快速发生。

简单不先于复杂,而是在复杂之后
对于一个简单的架构,总有人会想办法把它做复杂。踩了坑,下决心重构,成功则回归简单,失败就会被新的简单模式颠覆。踩坑本身也是有价值的,不然新人总是按捺不住还会再踩一次。做复杂很容易,但保持简单需要远见和克制。没有经历过过程的磨练,别人的解药对你可能是毒药。

架构不可能一成不变的,我们的图表最开始直接使用 D3、ECharts 很简单,后来定制很多逐渐复杂到难以维护,于是基于 G2 自研 bi-charts 后架构又一次变简单,前后的开发体验可能是差不多的,但背后的技术完全变了。

总结与展望

百万行代码没什么可怕,是一个正常的节点,仍然可以像几万行代码那样敏捷。

现在 Quick BI 已经向千万行迈进,向世界一流 BI 的目标迈进。以上内容更多是工程化相关,把工程化做好目的是想让开发者更专注于业务,没讲的业务挑战其实更多,因为数据分析天生就要与海量数据打交道,性能优化有长期的实践;洞察丰富异样的数据,有很多可视化及复杂表格方面的沉淀,可视化不仅是技术,也是业务本身;手机平板电视等多端展示,跨端适配的挑战。未来还希望能够把数据分析打造成一个引擎,能够快速集成到办公和商业流程中。

目前的开发模式并不完美,在迭代的过程中,不可避免会产生技术债,架构的优化本质就是在保持可维护性和减少技术债。最近团队在酝酿一次 Redux-Toolkit 的引入,会对取数和数据流有大的升级,有进展再分享。

相关实践学习
阿里云实时数仓实战 - 用户行为数仓搭建
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3 )前置知识要求:熟练掌握 SQL 语法熟悉 Linux 命令,对 Hadoop 大数据体系有一定的了解   课程大纲 第一章 了解数据仓库概念 初步了解数据仓库是干什么的 第二章 按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章 数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章 采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章 用户行为数据仓库 严格按照企业的标准开发 第六章 搭建业务数仓理论基础和对表的分类同步 第七章 业务数仓的搭建  业务行为数仓效果图  
相关文章
|
1月前
|
存储 监控 数据可视化
大模型可观测1-5-10:发现、定位、恢复的三层能力建设
本文通过丰富的代码Demo和截图为读者提供了可落地的实践指南。
416 34
大模型可观测1-5-10:发现、定位、恢复的三层能力建设
|
云计算
Matlab中读取txt文件的几种方法
Matlab中读取txt文件的几种方法 matlab读取文本文件的几种函数: 1、load——适合读取纯数据文本; 2、importdata——只读取数据,自动省略数据格式前后的字符,超大文件不适合; 3、textread、textscan——适合读取行列规整的文本,会存到元胞中,可通过he.
33770 0
|
XML Java 测试技术
Graalvm 替代 JVM 真的可以带来巨大的性能优势吗?
介绍 Spring Boot有助于轻松开发独立的、可用于生产的 Spring 应用程序。它对 Spring 平台和第三方库采用固执己见的方法:以最少的配置简化设置过程。优势: 易于使用:Spring Boot 简化了独立 Spring 应用程序的创建,无需复杂的配置。 嵌入式服务器:它允许直接嵌入 Tomcat、Jetty 或 Undertow 等服务器,从而无需单独部署 WAR 文件。 Starter 依赖项:Spring Boot 提供预配置的“starter”依赖项,降低了构建配置的复杂性。 自动配置:Spring Boot 自动配置 Spring 和第三方库,最大限度地减少手动设置工
|
9月前
|
机器学习/深度学习 人工智能 自然语言处理
人工智能在虚拟客服中的关键作用:提升交互体验与服务效率
人工智能在虚拟客服中的关键作用:提升交互体验与服务效率
543 90
|
前端开发 JavaScript
乾坤qiankun(微前端)样式隔离解决方案--使用插件替换前缀
乾坤qiankun(微前端)样式隔离解决方案--使用插件替换前缀
1870 8
|
11月前
|
数据库 Python
异步编程不再难!Python asyncio库实战,让你的代码流畅如丝!
在编程中,随着应用复杂度的提升,对并发和异步处理的需求日益增长。Python的asyncio库通过async和await关键字,简化了异步编程,使其变得流畅高效。本文将通过实战示例,介绍异步编程的基本概念、如何使用asyncio编写异步代码以及处理多个异步任务的方法,帮助你掌握异步编程技巧,提高代码性能。
279 4
|
机器学习/深度学习 存储 算法
R语言实现SMOTE与SMOGN算法解决不平衡数据的回归问题
R语言实现SMOTE与SMOGN算法解决不平衡数据的回归问题
272 1
R语言实现SMOTE与SMOGN算法解决不平衡数据的回归问题
|
Ubuntu Linux Windows
linux 挂载硬盘报错 "mount: unknown filesystem type 'ntfs'"
【10月更文挑战第7天】在Linux系统中挂载硬盘时遇到“mount: unknown filesystem type 'ntfs'”错误,是因为Linux默认可能不支持NTFS文件系统。本文提供了解决方案:安装NTFS-3G软件包以支持NTFS,并检查内核是否已加载NTFS模块。对于Ubuntu/Debian系统,可使用`sudo apt-get install ntfs-3g`命令;对于CentOS/RHEL系统,则需先安装EPEL仓库再安装NTFS-3G。此外,还需确认硬盘设备名正确无误,并创建合适的挂载点目录。
2726 2
|
NoSQL Shell MongoDB
【Python】已解决:(MongoDB安装报错)‘mongo’ 不是内部或外部命令,也不是可运行的程序
【Python】已解决:(MongoDB安装报错)‘mongo’ 不是内部或外部命令,也不是可运行的程序
1193 0
Element UI 带快捷编辑的多行输入框(含光标位置的获取和指定)
Element UI 带快捷编辑的多行输入框(含光标位置的获取和指定)
186 0