高级工程师的晋升之路:如何用 JavaScript 打造十亿级的应用
Posted CSDN
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高级工程师的晋升之路:如何用 JavaScript 打造十亿级的应用相关的知识,希望对你有一定的参考价值。
这是我在澳大利亚JSConf上的演讲稿,经过少许编辑。
我以前开发过超大规模的javascript应用。现在我不做了,所以我觉得应该回顾下我学到的东西。昨天我在宴会上喝啤酒时有人问我,“嗨Malte,你为什么要来讲这个话题?”
我觉得这个问题的回答也属于这次演讲的范围,虽然我觉得讨论自己的事情感觉有点奇怪。
言归正传,我在Google开发了这个JavaScript框架。Photos、Sites、Plus、Drive、Play和搜索引擎所有这些网站都用到了我的框架。有些网站的规模非常大,估计你也用过 。
这个JavaScript框架不是开源的。不开源的原因是,它跟React差不多是同时出来的,我当时有种“既生瑜何生亮”的感觉。
Google已经有好几个框架了,像Angular和Polymer,我觉得再有一个框架会让人难以选择,所以我觉得我们还是留给自己用就好了。
但除了没有开源之外,我感觉从这个框架中学到了很多东西,有必要在这里跟大家分享一下。
超大规模应用,以及这些应用的共同点
我们来讨论下超大规模应用,以及这些应用的共同点。当然,肯定有很多人一起开发。可能还有更多的共同点,很多是人的问题,他们有感情,有人际交流的问题,所以你必须得考虑到。
就算你的团队没那么大,也许你只是在其中工作了一段时间,甚至可能你并不是第一个负责维护的人,你可能也没有所有需要的信息。
也许有很多你并不理解的东西,也许团队里其他人也不理解应用的方方面面。那么,我们在构建超大规模应用时,必须要考虑下面的内容。
(只有高级工程师没有初级工程师的团队,只是工程师团队而已)
这里我想说的另一件事就是,从职业生涯的角度为这次演讲提供一些背景。我估计许多人都认为自己是高级工程师。或者我们虽然还不是,但希望有一天能成为高级工程师。
我认为,成为高级工程师意味着能够解决别人给我的几乎所有问题。我精通我用的工具,精通我的专业领域。而工作的另一个重要部分就是,我要让其他初级工程师最终成长为高级工程师。
(初级 -> 高级 -> ?)
但事实上,有时候我们会想“下一步该干什么了?”当我们到达高级阶段后,下一步又该怎么走?
一些人可能想进入管理层,但我不认为每个人都希望如此,因为肯定不可能所有人都是经理,对吧?一些人非常擅长工程,为什么不能一辈子干工程呢?
(“我知道我能解决这个问题”)
我想建议一条适合高级工程师的晋级之路。当我说我自己是高级工程师时,我会说“我知道我能解决这个问题”,而且因为我自己知道该怎么解决,我也能教别人该怎么解决。
(“我知道别人会怎么解决这个问题”)
我的理论是,下一级别应该是“我知道别人会怎么解决这个问题”。
(“我能预料到API的选择和抽象会怎样影响到其他人解决问题的方法。”)
我们来具体说一下。比如这句话:“我能预料到我做出的API选择,或者我引入到项目中的抽象,会如何影响到其他人解决问题的方法。”
我觉得这个概念十分有用,能够让我判断我的决定会怎样影响到应用程序。
(共情的应用)
我称这种为“共情的应用”。你要和其他工程师一起思考,并且要思考你的行为和你给他们的API会怎样影响他们写软件的方式。
共情的应用
(简单模式下的共情)
幸运的是,这里说的共情是简单模式的。共情通常很难,这里的所说的共情也很难。但至少你需要共情的那些人也是软件工程师。
尽管他们跟你会有很大区别,但至少在构建软件方面和你有很多共同点。在有经验之后,这种共情很容易做得很好。
(编程模型)
仔细想一下这些话题,我只想谈一个真正重要的词,那就是编程模型——这个词我以后会经常提到。
它的意思是“给定一组API,或者一组库,或者框架,或者工具,人们会怎样用这些东西编写软件。”我的演讲的真正内容是,API等的变化会怎样影响编程模型。
(影响编程模型的例子: React,Preact,Redux,来自npm的Date picker,npm)
我举几个影响编程模型的例子。假设你有个Angular项目,如果说要把这个项目移植到React上,这显然会影响到人们编写软件的方式,对吧?但如果说"60KB就能操作虚拟DOM,我们切换成Preact吧”,由于这个库是API兼容的,因此你做的这个决定不会影响到他人怎样编写软件。
也许之后你会说“这些都太复杂了,我们得有个工具来管理应用的工作。我们要引入Redux。”这就会影响到别人编写软件的方式。如果有个需求说“我们需要个日期选择控件”,结果在npm上搜了下发现了500个,于是你选了其中一个。
那么选哪个会有影响吗?肯定不会影响编写软件的方式。但是,手头有npm这个工具,那么琳琅满目的模块肯定会影响到你写软件的方式。当然,还有许多其他会影响或不会影响人们编写软件方式的例子。
“代码分割”的技术
现在谈一下所有大规模JavaScript应用在发布时的共同点之一,那就是由于项目非常庞大,我们不希望一次性发布所有部分。因此我们引入了一种叫做“代码分割”的技术。
代码分割的意思就是把应用做成多个包(bundle)。因此,如果一些用户只需要使用应用的这个部分,另一些用户只使用另一个部分,我们可以把应用分成几个包,这样用户只需要下载他实际会用到的那部分应用程序。
这个技术是我们都会做的。和许多其他东西一样,这种技术是由闭包编译器发明的——至少在JavaScript的世界中如此。不过我认为实现代码分割的最常见的办法就是使用webpack。
但如果你用的是RollupJS——这个库也很棒,他们最近也开始支持代码分割了。代码分割肯定是要做,但在引入代码分割时一定要谨慎,因为它会影响到编程模型。
有了代码分割,以前的同步的东西就变成了异步的。没有代码分割到时候应用很简单明了。程序加载之后就稳定了,无需再等待任何东西。
但有了代码分割,有时候就得因为“需要加载某个包”而访问网络,就得考虑这样做带来的影响,应用程序会变得更复杂。
此外,这里面也有人的参与,因为代码分割需要把包定义好,还需要你去思考什么时候该加载某个包,因此你的团队里的工程师必须要决定,哪些文件放进哪个包里,什么时候加载这个包。只要有人参与,就会影响到编程模型,因为他们得思考这些东西。
(根据路由进行代码分割)
有个经过实践考验的方法能解决代码分割问题,这样人就不用考虑代码分割问题了。这种方法叫做“基于路由的代码分割”。如果你还没有使用代码分割,那你可以从这种方式入手。路由就是应用程序URL结构中的基础部分。
例如,产品页面位于/product/下,而分类页面位于别的地方。你可以把每个路由做成一个包,这样应用里的路由程序就能知道代码分割了。
当用户访问某个路由时,路由器就会加载相应的包,然后这个路径就不需要人去操心了。
现在的编程模型就跟刚开始只有一个大包的情况没什么太大区别了。这种方法很好,应该从这里开始入手。
但这次演讲的标题是“超大规模JavaScript应用”,应用的规模迅速扩大,导致每个路由一个包已经无法实现了,因为路由本身也变得非常大。关于什么是超大规模应用我有个很好的例子。
(Google搜索上查询“public speaking 101”的结果)
在这次演讲之前我搜索过怎样在公众场合讲话,于是我得到了这一堆蓝色的链接。可以很肯定,这个页面只需一个单一的路由包就可以处理。
(Google搜索上查询“weather”的结果)
但稍后我想知道天气,因为加州的冬天很冷。结果立刻就跳到了完全不同的模块。看起来这个简单的路由比我们想像的要复杂。
(Gogole上搜索“20 usd to aud”的结果)
然后我收到会议邀请后,想查一下美元到澳元的汇率,结果显示了复杂的货币转换工具。
显然,这种特殊模块有上千个,显然不可能把所有模块都放到一个包里,否则这个包就会变成几个兆,用户下载起来会很困难。
(组件级别的懒加载?)
因此,我们不能简单地根据路由进行分割,必须找其他的办法。基于路由的代码分割很容易,因为这是最粗糙的分割方式,更深入的部分可以忽略。
我喜欢简单的东西,那么如果在细粒度上进行代码分割会怎样呢?考虑下如果对每个组件都进行懒加载会这哪一个。
如果只从带宽效率的角度来看似乎很不错。但从其他角度考虑,比如延迟,这却是个很糟糕的想法,但这种想法是值得考虑的。
(React组件静态地依赖其子组件)
但想像一下,假设你的应用使用React,而React应用静态地依赖于子组件。也就是说,如果因为要懒加载子组件就打破这种依赖,那就改变了编程模型,以后的事情就没那么容易了。
(ES6 import的例子)
如果你有个货币转换组件,希望放在搜索页面上,那肯定要import对吧?通常用ES6模块实现:
(可加载的组件的例子)
但如果想懒加载,代码就会变成这个样子,使用动态import懒加载ES6模块,并封装到一个可加载的组件中。
当然,实现方式数不胜数,而且我并不是React专家,但这种方式肯定会改变你编写应用的方式。
以后的事情就没那么简单了。静态的东西成了动态的,这是另一个编程模型改变的标志。
你会马上问:“由谁来决定什么时候懒加载哪个模块?”因为这个问题的答案会影响到应用程序的延迟。
于是又要涉及到人了。人需要思考“这儿有个静态import,还有个动态import,什么时候该用哪个呢?”搞错这个会很糟糕,因为本来是静态import的东西被动态import的话就会在包里加载不该加载的东西。这种事情,在项目持续很长时间,经历过很多工程师的手之后,就一定会出错。
现在我来说说Google怎么做到这一点,以及如何在保证良好性能的前提下实现优秀的编程模型。
我们的做法是,将组件按照渲染逻辑、应用逻辑分割。这里所说的逻辑就是按下货币转换工具上的按钮这种逻辑。
(只加载会被渲染的逻辑)
现在有两个分割好的东西,我们只加载之前渲染过的组件中的应用逻辑。这个模型非常简单,因为只需要做服务器端渲染,然后不管渲染的是什么,只需下载相关的应用包就可以了。这样就把人的因素排除在了系统之外,因为加载是通过渲染自动进行的。
“注水”
这个模型看起来似乎不错,但它需要付出些代价。如果你了解React或者Vue.js等框架中的服务器端渲染的典型做法的话,你就应该知道它们的做法叫做“注水”(hydration)。
注水的原理是,服务器先进行渲染,然后客户端再进行同样的渲染,也就是说前端需要加载代码以渲染那些已经存在于页面上的东西,因此无论加载这些代码还是运行它们都是显著的浪费。
它会浪费大量带宽和CPU,但它非常好用,因为你在客户端不用考虑服务器端已经渲染过什么了。我们在Google用的方法不一样。
因此,如果要设计一个超大规模的应用,你就得考虑:是要用超级快但超级复杂的方法,还是直接使用注水方式,虽然效率低一些但编程模型却很舒服的方法?你必须得作出决定。
下一个话题是我最喜欢的计算机科学问题之一——不是关于明明的,尽管我估计我起的名字很糟糕。
它的名字是“2017节日特别问题”。有没有人有过这种经历,以前写的代码,现在虽然不再使用了,但还留在代码库中?
大家都知道这个问题,而且最严重的就是CSS。一个超大的CSS,里面有各种选择器。谁知道哪个选择器还有用?所以干脆留在里面好了。
我觉得CSS社区正面临着革命,因为他们也意识到了这个问题,因此他们想出了像CSS-in-JS等解决方案。
用这种方案,一个组件只需要一个文件,比如2017HolidaySpecialComponent组件,只要裹了2017年,就可以把整个组件都删掉,于是所有东西就都没了。这样删代码就变得特别容易了。这个点子很好,应该不仅仅是CSS使用这种方式。
(尽一切努力避免集中化配置)
如何避免集中化配置
我想举几个例子说明避免集中化配置这个思想,因为集中化配置会让删除代码变得极其困难,比如集中化CSS文件等。
(routes.js)
之前我说过应用程序里的路由。许多应用程序会有个名为routes.js的文件,里面包含了所有路由,路由将自己映射到某个根组件上。这就是集中化配置的例子,这种情况在大规模应用中应当尽力避免。
因为集中化配置会造成这种情况:有工程师会问“那个根组件还要不要?我需要更新那个文件,但它属于别的团队。不清楚我是否应该修改它。或者留着明天再说吧。”由于这样的原因,人们只敢向这个文件里添加内容了。
(webpack.config.js)
另一个例子就是webpack.config.js文件,通常这个文件被用作构建整个应用。短时间内可能没问题,但最终,由于这个文件必须了解所有其他团队在应用中的工作的细节,所以没办法伸缩。同样,我们需要一种模式,在构建过程中使用去中心化的配置。
(package.json)
另一个例子就是package.json,这是npm所用的文件。每个包都说“我有这些依赖,我的运行方式是这样,编译方式是这样”。但显然不可能存在一个巨大的配置文件适合所有的npm包。
它没办法处理几十万个文件。因此,在git中就会导致许多冲突。没错,这的确是因为npm太大了,但我认为我们的许多应用也会变得那么大规模,从而我们不得不考虑同样的问题,采用同样的模式去解决。
我并没有所有的解决方案,但我认为CSS-in-JS带来的思想也可以用在应用程序的其他方面。
用更抽象的形式来描述这种思想,就是我们要对应用程序的抽象设计思想和组织形式负责,即对应用程序的依赖树的形状负责。
这里所说的“依赖”是指抽象意义上的依赖,它可以是模块依赖,可以是数据依赖,服务依赖,有很多种。
(依赖树的例子,包含路由器和三个根组件)
显然,所有这些应用程序都超级复杂,但我这里举个非常简单的例子。它只有四个组件。
它包含一个路由器,路由器知道路由之间的转移。此外还有几个根组件A、B和C。
(中心化import的问题)
前面提过,这种模式有中心化import的问题。
(import -> enhance)
在Google,我们想出了个办法来解决这个问题。我以前应该没说过这个方法,所以在这里介绍一下。我们发明了一个新概念,叫做“增强”(enhance)。我们用它来替换import。
实际上,它是import的另一面。它是反向依赖。enhance一个模块的意义就是让那个模块依赖你。
从依赖图中可以看出,组件还是那几个组件,但箭头的方向是反的。因此,我们没有让路由器导入根组件,而是让跟组件声明,自己会增强路由器。
这样,删除一个根组件只需要删除文件就可以了,因为这个根组件不再增强路由器,所以删除根组件需要的唯一操作就是删除文件。
(谁来决定何时使用enhance?)
这种方法很好,直到又遇到了人的问题。现在开发者得考虑“我是该用import,还是用enhance?什么情况下该用哪个?”
这正是这种方法的弊端。由enhance功能过于强大,它可以让系统中的所有模块都依赖于你,如果被错误使用,这是非常危险的。
不难想像,这会导致非常糟糕的结果。因此在Google,虽然我们确定这是个很好的想法,但最终我们认为它不应该被使用,任何人都不应该使用这种模式,除了一种情况之外:自动生成的代码。
实际上,这种模式非常适合自动生成的代码,它能解决一些生成代码的固有问题。生成代码的时候,有时你得导入一些看不到的文件,有时得猜测它们的名字。
但是,如果生成的文件只是在默默增强它们需要的组件的话,就没有这些问题了。你永远不必去理解那些文件,它们只是在增强着整个代码。
(单一文件组件指向它的各个组成部分,各个组成部分都enhance路由器)
我们来看一个具体的例子。上面是个单一文件的组件。在该组件上运行代码生成器,然后从中提取出路由定义文件。
该文件说“嗨路由器,我在这儿,请import我”。显然,这种模式可以用在所有其他东西上。如果你在使用GraphQL,并且需要路由器知道数据依赖,那么就可以使用同样的模式。
不幸的是,我们需要知道的并不只这些。下面是我第二喜欢的计算机科学问题,我称它为“base包(bundle)的垃圾堆”。在应用程序的包的构成图中,base包是那个永远会被加载的包,不管用户需要使用的是应用程序的哪个部分。
因此,它极其重要,因为如果它过大的话,那么在它之下的一切东西都会过大。如果它比较小,那么至少依赖它的包还有机会小一些。
一个小笑话:我加入Google Plus JavaScript基础设施团队后,发现他们的base包有800KB的JavaScript。
所以我的忠告就是,如果你想比Google Plus更成功,不要让你的base包超过800KB的JS。很不幸,这种糟糕的状态很容易达到。
(base包指向三个不同的依赖)
举个例子。base包需要依赖于路由,因为从A转移到B时,B的路由必须是已知的,所以base包里需要包含路由。但是,base包里不应该包含任何UI代码,因为用户从不同的途径打开应用可能会看到不同的UI。
因此,比如日期选择器绝对不应该在base包里,结账的工作流也不应该在base包里。但怎样防止这一点呢?很不幸,import十分脆弱。你导入了util包(package),因为它里面有个函数可以生成随机数。
后来有人说“我需要给自动驾驶汽车写个工具函数”,于是你的base包里就导入了为自动驾驶汽车准备的机器学习算法。这种事情很容易发生,因为import会传染,因此时间一长就很容易产生垃圾。
我们的解决方案叫做“禁止依赖测试”(forbidden dependency tests)。这种方法可以检查比如base包是否依赖任何UI。
来看个具体的例子。在React中每个组件都需要和React交互。因此,如果目标是base包不包含任何UI,那么只需要增加这样的断言:React.Component不是base包的依赖。
(划掉的是被禁止的依赖)
再看一下前面的例子。如果有人试图添加日期选择器,就会导致测试失败。而且这些测试失败通常很容易修复,因为大多数情况下那个人并不是有意添加那个依赖的,只是通过某种传染途径进去了而已。
相比之下,如果一个应用没有类似的测试,并且一个依赖进入了两年之久,那就很难通过重构来删除这个依赖了。
理想情况下,你会找到最自然的那条路径。
(最直接的路一定是正确的路)
最理想的状态是,不管团队里的工程师做什么,最直接的路永远是正确的路,这样他们就不会走错路,从而自然而然地做正确的事情。
(否则,写个测试来保证正确的路)
但这种状态并不一定可行。如果不可行,就写个测试。不过许多人并没有动力写测试。
但是,请务必给应用程序写测试,来保证基础设施不会被改变。测试不仅是要测试数学函数是否正确。测试也可以用于应用程序的基础设施和主要设计上。
(避免在应用程序之外依赖人的判断)
在应用程序之外,尽量避免依赖于人的判断。编写应用程序时,我们要理解业务,但并不是公司里的每个工程师都能理解代码分割的原理。
而且他们也不需要知道。在导入这些东西时,要保证即使他们不理解也能正确使用。
(让删除代码更容易)
真的,要让删除代码更容易。我的演讲叫做“创建超大规模JavaScript应用程序”。
我的最好的建议就是:不要让应用程序变得太大。做到这一点的最好方法就是及时删除东西。
(没有抽象要好过错误的抽象)
我还想说一点,那就是一些人认为的没有抽象要比错误的抽象更好。这句话的真正含义是,错误抽象的代价非常高,因此一定要小心。
我觉得这句话有时候被误解了。它并不是说我们不应该要抽象。它只是说你要格外小心。
我们要善于找到正确的抽象。
(共情和经验 -> 正确的抽象)
正如我在演讲开头提到的:最好的办法就是通过共情,想像下团队里的工程师会怎样使用你的API,怎样使用你的抽象。有了经验,就能知道怎样使用这种共情。综合起来,共情和经验能让你给应用程序选择正确的抽象。
原文:https://medium.com/@cramforce/designing-very-large-javascript-applications-6e013a3291a3
译者:弯月;责编:胡巍巍
如果你有优质的文章,或是行业热点事件、技术趋势的真知灼见,或是深度的应用实践、场景方案等的新见解,欢迎联系 CSDN 投稿,联系方式:微信(guorui_1118,请备注投稿+姓名+公司职位),邮箱(guorui@csdn.net)。
————— 推荐阅读 —————
以上是关于高级工程师的晋升之路:如何用 JavaScript 打造十亿级的应用的主要内容,如果未能解决你的问题,请参考以下文章