作者:Filip Hracek
编译:胡子大哈
翻译原文:http://huziketang.com/blog/posts/detail?postId=58abfab1204d50674934c3a9
英文原文:How Google builds web frameworks
** 转载请注明出处,保留原文链接以及作者信息**
![](https://huzidaha.github.io/images-store/201702/41-QDS-kCgeF8ZJg_JSEwwIeA.jpeg)
众所周知,Google 使用单一仓库来共享所有代码——20亿行代码,并且这个仓库是采用基于 trunk 的开发方式。
![](https://huzidaha.github.io/images-store/201702/42-3hPZNDocbp68XsbsJoZ-iQ.jpeg)
(这毫无疑问是世界上最大的代码仓库。)
对于 Google 公司以外的开发者来说,这是令人惊讶的,也是反直觉的,但是它确实运作良好。(所给的链接 文章 中已经给了很好的例子,这里就不累述了)
Google 的代码库在 Google 在全世界几十个办事处,共 25000 多名软件工程师之间共享。在一个普通的工作日里,就有 16000 多次代码提交。
这篇文章主要讲述构建一个开源 Web 框架(AngularDart)的开发过程。
![](https://huzidaha.github.io/images-store/201702/43-42xyxKFKI9a0j0BWuHGIHg.jpeg)
(“Human users” 意味着谷歌的软件工程师提交代码)
只有一个版本
当你在一个单仓库项目中使用基于 trunk 的开发模式时,你的所有东西都只有一个版本。例如, Google 不会发生一个叫做 FooBar 的 App 使用 AngularDart 2.2.1 版本,而另一个叫做 BarFoo 的 App 使用 2.3.0 版本。所有的 App 都用同一个版本——而且是最新版本。
![](https://huzidaha.github.io/images-store/201702/44-vdQqatZdTxZ9CUDs-.png)
(基于 trunk 的开发模式时示意图,来源:trunkbaseddevelopment.com )
这就是为什么 Google 的工程师说所有的 Google 软件都活在“失血边缘”。
如果现在你的心里在大喊“太危险了!”,那就对了。在你的生产环境代码里面依赖另外一个库的 trunk(相当于 git 里面的 master)分支上面的代码听起来确实很危险,但是你要知道,故事在这之前还有别的情节。
每次 commit 前的 74000 次测试
AngularDart 定义了 1601 个测试(这里)。但是当你要提交一个改动到 Google 仓库中的 AngularDart 源代码时,所有依赖于 AngularDart 的测试都会被跑一遍。这时,大概有 74000 个测试(这依赖于你的改动有多大——系统会知道你影响了哪些测试,并且会启发式地跳过这些测试)。
![](https://huzidaha.github.io/images-store/201702/45-5VjjBOiVq74495vLAKctOg.png)
测试越多越好。
例如,我仅仅是做了一个小改动,用来在变化检测插入验证算法中做一个模拟某种场景的竞争情况(我添加了&& random.nectDouble() > .05
到 这条 if 语句 中)。当我运行它的时候(运行一次),AngularDart 本身的1601个测试并不需要跑,但是这个改动却命中了一堆的客户端测试。
这里面体现出真正的价值是,这些测试都是真实产品的测试。这些测试不仅数量庞大,同时它们也反映了开发者是如何使用的这个框架(不仅仅是框架的作者使用)。这样做的意义是:框架的作者并不总是能够正确地预估别人是怎么使用他的框架。
对正在生产环境运行的 App 也是有好处的,毕竟上面每个月有数十亿美元的流水。开发者不是在一个业余时间拿来玩玩的 Demo App 上使用框架,而是一个成千上万人投入在上面的线上产品使用。如果 Web 是和未来息息相关的,那么作为 Web 框架开发者,我们更应该更好地支持后者的开发。
![](https://huzidaha.github.io/images-store/201702/46-DrJBfzzSTkGdmrlu6OnYfA.png)
所以,如果一个框架导致依赖于它的一些 App 崩溃,会发生什么?
谁搞砸,谁修复
如果 AngularDart 的某位成员引入了一个引起崩溃的改动,那么他必须要为他们的用户修复它。因为大家都使用单一的仓库,因此 bug 很容易被发现,并且可以立马修复它。
因为 bug fix 的时候可能带来新的 bug,因此 bug 和 fix 有可能是同时入代码库。当然,在入库之前它们都会被做代码审查。
我们来给一个具体的例子。当 AngularDart 团队里的某个成员修改了代码影响了 AdWords 程序的运行,那么他们就要跑到 AdWords 去修复它们。在修复代码过程中,他们会跑 AdWords 已有的测试,也可以新建测试。然后他们会把测试和 bug fix 都放入变更列表,并且申请代码检查。因为这个变更列表中包含了 AngularDart 代码和 AdWords 的代码,因此系统会自动地将代码发送到两个团队进行审查,只有两边都通过了才能被提交入库。
![](https://huzidaha.github.io/images-store/201702/47-kbwhvH4lz1B-jRHBCEvAcA.png)
这是一个很好的防止闭门造车的方法。AngularDart 框架的开发者们可以访问到数以百万计行的代码,这些代码都依赖于该框架。他们不需要去假想其他人会怎么使用这个框架。(当然他们只能访问到 Google 的代码,而访问不到世界上其他依赖于 AngularDart 的代码。)
别人用你的框架,但是你要升级别人的代码,这样听起来会使得开发变慢。但是也没想象中那么慢(可以看一下 AngularDart 10月份的进度),但是也确实让开发进度慢了一些。这是一把双刃剑,怎么来看待这个问题取决于你想要从这个框架中获得什么。等下我们再来讨论这个问题。
所以,如果有个 Google 的家伙跟你说,他们 Alpha 版本的库已经稳定了并且可以应用到生产环境了,你就知道是什么回事了。
大规模改动
那么,如果 AngularDart 要做一个大规模的变动(如版本从 2.x 升级到 3.0)并且会命中 74000 个测试该怎么办呢?团队要修复所有的问题吗?难道他们要去更改上千个跟根本就不是他们写的源文件吗?
是的。
一个好的 类型安全系统 会使你的工作更有效率。例如,在一个类型安全的 Dart 中,类型安全可以保证所有变量都有一个确定的类型。那么你进行重构的时候很多东西都可以做到自动化,而不需要开发者手工确认。
当类 Foo 中的一个方法bar()
变成了baz()
,你可以创建一个工具,它可以遍历整个 Google 仓库,找到所有 Foo 类的实例以及它子类的实例,并且把所有的bar()
改成baz()
。有了类型安全,你可以确定这个改动不会使任何地方崩溃。如果没有类型安全,甚至这么一个简单的修改都可能变得非常麻烦。
![](https://huzidaha.github.io/images-store/201702/48-yxqdl9CBoB48XG0avf4piQ.gif)
(根据 Dart 代码规范,一键格式化代码)
另外一个对于大规模改动很有帮助的是 dart_style, Dart 的默认格式器。所有 Google 的 Dart 代码都是通过这个工具进行格式化的。在你的代码被 push 到代码审查人员之前,就通过 dart_style 进行格式化了,所以不存在像要不要把新的一行放在这或者放在那这样的问题。这也被应用到大规模的代码重构。
性能指标
正如我上面所说的,AngularDart 得益于依赖于它的产品的测试。但仅仅只是测试还不够,Google 对于 App 的性能要求的很严格,几乎所有的产品都有基准测试套件。
所以,如果 AngularDart 引入了一个导致 AdWords 加载慢了 1% 的改动,上线这个改动之前他们都会知道。如果十月份 AngularDart 团队宣布他们自从八月份以来 AngularDart App 比原来体积小了40%,速度快了10%的时候,他们并不是在讨论什么 TodoMVC 这样的小事。他们谈论的是真实世界中里面上百万级别用户的产品和上兆字节的业务逻辑代码。
![](https://huzidaha.github.io/images-store/201702/49-FFPofhArfE_q-ppyTkDniA.png)
顺带提一下:封闭式编译工具
你可能会好奇,在 AngularDart 引入了一个 bug 后,在这么庞大的内部仓库里面,应该去跑哪些测试呢?当然不会手动的在 74000 个测试去挑测试来跑,当然也不会把 Google 所有的测试都跑一边。答案在于一个叫做 Bazel 的玩意儿。
在这种代码规模下,你不可能写 Shell 脚本来编译所有文件,这样做会很不靠谱而且会慢的令人发指。这时候你需要的是一个封闭式编译工具。
“封闭式”(Hermetic)的意思有点类似于函数式编程里面纯函数的”纯“。也就是说,你的编译过程不能有副作用(如临时文件,改变环境变量 PATH 等),他们也必须是确定性的(相同的输入必须得到同样的输出)。这样的话不管你什么时候在你的机器上编译和运行测试,你会得到一致的输出结果。你不需要make clean
。因此你可以发送你的编译/测试到编译服务器上并行编译。
![](https://huzidaha.github.io/images-store/201702/410-sq_8UFpeBsxSIpBXpmWiSg.png)
Google 花费了数年的时间开发这个编译工具。近些年将其开源了,就是 Bazel。
多亏了有这样的基础设施,内部测试工具才能够自己来决定编译和测试哪些受影响的部分,并且在恰当的时候运行他们。
以上种种意味着什么?
AngularDart 的目标非常明确,就是要在构建大型 Web 应用领域,做到一流的效率、性能和可靠性。这篇文章所讲述的就是后一个部分——可靠性。同时也解释了为什么 Google 的关键产品,如 AdWords 和 AdSense 都使用这个框架。不是 AngularDart 自己吹嘘自己有多么厉害的用户,就正如上面所说的——就是因为有这么多的内部用户,AngularDart 几乎不可能引入一些随意的变更。这也使得这个框架更加的可靠。
![](https://huzidaha.github.io/images-store/201702/411-BjhLEoihrMr6eRcTYL50ag.png)
(如果你觉得这些听起来太商业化了,你可以看一下我的非商业化 AngularDart 项目 自动化的特朗普 或者 Prime Finder)
如果你想要一个每几个月就有一次大的改版或者重大功能升级的框架,很明显 AngularDart 并不是你要找的。即使我们想这么干,你看完这篇文章就只知道,这么严格的开发流程是不可能开发出这样的框架的。但我们真心相信,一个不是很流行但是却很稳定的框架肯定会它有发展和生存的空间。
在我看来,如何判断一个开源技术会不会得到长期的维护和支持的,那么你就看看这个技术是不是维护者的公司业务的重要组成部分。可以拿 Android,dagger,MySQL 或者 git 作为例子。这就是我为什么我非常高兴地看到 Dart 有一个首选它的 Web 框架(AngularDart)、一个首选它的组件库(AngularDart Components)、一个首选它的移动框架(Flutter)——所有的这些都被用 Google 的商业产品中。
我最近正在写一本《React.js 小书》,对 React.js 感兴趣的童鞋,欢迎指点。