1.5 大型系统开发
1.5.1 框架与架构是不同的
我加入阿里巴巴工作的时候,前端并不算在技术部门:当时的支付宝,前端是产品部里的一个小型团队。而我也并不负责前端方面的工作—事实上直到现在,我也并没有在前端部门中工作过—我当时收到的Offer是业务架构师。
那正是在国内前端研发开始崛起的时候。我参加了2008年的D2大会(Designer&Developer Frontend Technology Forum),记得那时,Hedger主讲的话题就是Yahoo的前端框架YUI。而在随后的SD2C大会(Software Development 2.0 Conference)上,我将之前在JavaScript、Delphi和Erlang三种语言上的研究打包,通过“Erlang in Delphi”项目公布了出来。差不多直到2010年,我在前端方面的主要工作集中在一些商业推广和技术推动性质的大会上。类似地,这一年的QCon大会北京站,Douglas Crockford带来的《JavaScript的现状和未来》,也事实上代表了ES5在国内的正式推动。
与2008年的D2大会仅仅相距两年,整个行业已经将话题从“一种框架的使用”转向了对JavaScript的前途与命运的探讨上。缘于我的工作性质,我得以有更多的时间来与前端讨论架构问题,例如,什么是架构、框架与库,以及它们的实战。不过即便如此,我仍然一直游离于前端工作之外,唯一一次对前端的影响,大概是在Kissy.js的评审和重构中为核心架构添加了seed的概念(即具有meta性质的host对象)。就是因为这件工作的无心插柳,我的名字也被记入这个项目的交付历史中。这算是我从WEUI、Qomo、QoBean等项目一路行来,多年来所思所得的最后一点印记。
正是在这些看似与前端不着边际的工作中,我渐渐了解到前端对框架和架构有着独立的、深刻的而又迫切的需求。然而这时的Qomo项目却是实实在在地停下来了:一方面我没有了语言探索上的动力,另一方面也缺乏将它们应用于大型项目的推动能力与业务环境。因为Qomo与QoBean在本质上是探讨了JavaScript这门语言的一种扩展模式,即基于语言自身的原子特性进行二次实现的能力。这种能力是代码级别的组织变化,而对工程对象(即具体的项目)影响很小。
我只是在探索JavaScript这门语言的边界,然而现实却走向了工程化的大型系统开发。
1.5.2 大型系统与分布式的环境
最先对“工程化”做出响应的就是林林总总的“模块加载”方案。这些方案既尝试通过类似名字空间的方法来解构大型系统的复杂性,又试图对JavaScript的全局环境进行再次规划。不过归结起来,所有的努力其实都是在为“ES5否定了ES4”这件事偿还技术债。
然而我所面临的问题却与此不同。这时我已经辞去在支付宝的工作,在2013年4月加入了豌豆荚。在很长的一段时间里,我工作在后端,并致力于解决资金、账户等系统中的风控问题。更确切地说,关键的架构决策是如何在Nginx环境下寻求一个高效的技术方案,以使风控系统对其他业务系统的影响最小。而我最终的选择是尝试将整个服务端环境理解为统一调度的可计算资源,将每一个风控对象(例如资金操作的行为路径或用户账户)理解为一个可计算节点,并将这一切构建在一起变成一个实时计算的集群。这样一来,每一个风控对象的行为(数据的或用户的等)在这个集群节点的任意位置都可以得到处理,例如推送风控告警或者实施系统决策的风控措施。
于是我得到了一个称为N4C的分布式架构,它看起来与JavaScript没什么关系,因为它其实是基于Nginx上的Lua来实现的。不过从一开始,N4C就借鉴了ECMAScript实现并行系统的思想:使用Promise。为此我还专门完成了Promise类在Lua上的实现。当然,对前端历史了解得多一些的读者也会知道,这也是我后来被卷入“红绿灯大战”的缘由。
随后我将N4C发布为一个通用分布式并行架构的规范,在这个规范下分别交付了它的Lua和JavaScript实现。后者就是被称为Sandpiper的项目,它使用etcd实现集群中的核心数据存储和心跳通知。并且,(当然,)它使用Node.js作为运行环境。
到了2016年上半年,也就是在这个风控系统以及相关的开源项目发布之后不久,豌豆荚被阿里巴巴收购了。我没有回到阿里巴巴,而是来到了一家旨在简化智能硬件开发的互联网公司(ruff.io)。在Ruff项目中,我们试图将JavaScript编译到嵌入式操作系统的内核,以便在芯片或模组级别提供类似Node.js的开发环境,并且最终使JavaScript引擎在这样的操作系统环境下解释执行。基于这个理念,Ruff提供了一个与Node.js+NPM类似的开发环境,以及远程操作嵌入式系统内核的方式—最后,将这一切集成在开发板上。
然而这与互联网有什么关系呢?我随后需要解决的问题正在于此:如何将一个开发板(以及它所代表的、设备化的物联网络)带入互联网环境中。我提出了称为Sluff的项目,并在这个项目中再一次启用了Promise这个“大杀器”,以实现各个物联设备的并行计算和集中调度。有了Sluff,就可以将一个或多个子级的物联网理解为全局的、整体的物联网的一个节点,通过各自的、边缘化的计算来交付数据或响应网络消息。Sluff的核心在于管理这些设备的抽象(将它们映射为逻辑对象),并让设备中的行为变成一个个Promised的数据、时间点、动作或者失效处理。
ES5之后的十年,无论是金融级的风控系统还是物联网环境,我所见到的,是JavaScript在不停地向它周边的领域渗透。不可否认,是ECMA赋予了JavaScript新的能力和活力,然而JavaScript自身作为混合语言的原始设计与思想,仍然是这门语言的核心所在。
1.5.3 划时代的ES6
在ES5成功发布之后,JavaScript也迎来了它的好时代。一方面,ES5总算对已有的那些JavaScript实现版本中存在的诸多不同现象给出了结论,另一方面则从规范层面杜绝了在“多种特性的进化路线”这一问题上选边站队—尽管其结果是什么选择都没有做,但实际上也是开放了各种选择的可能。
得益于TC39工作小组的创新性的努力以及强大的包容性,许多在社区中的声音被关注到,大量提议作为语言的新特性得以采纳。ECMAScript走上了一条不同以往的进化之路,并在尽量满足社区愿望和控制语言一致性之间取得了平衡。通过各种shims或第三方包,新的语言特性在规范定义之前就被开发人员“尝鲜”,因此再将这些特性写进规范就几乎不会碰到什么阻力。甚至有些语言特性在还没有进入规范的提案阶段,就已经成了立时可用的、流行的事实标准。
社区强大而丰富的能量灌注以及TC39孜孜不倦的努力,终于联手打造了ECMAScript史上最大的一次升级:在2015年6月,ES6发布了。这个ECMAScript版本几乎集成了当时其他语言梦寐以求的所有明星特性,并优雅地、不留后患地解决了几乎所有的JavaScript遗留问题—当然,其中那些最大的、最本质的和核心的问题其实都已经在ES5推出时通过“严格模式(strict mode)”解决了。
ES6提出了四大组件:Promise、类、模块、生成器/迭代器。这事实上是在并行语言、面向对象语言、结构化语言和函数式语言四个方向上的奠基工作。相对于这种重要性来说,其他类似于解构、展开、代理等看起来很炫很实用的特性,反倒是浮在表面的繁华了。随后各大引擎纷纷对这一新版本规范明确表明欢迎和支持,其中类似Chrome V8这样的引擎由于一开始就参与规范的制定工作,所以大量特性已经在先期版本中发布过了,因而也收获了满满的技术红利。尽管一部分应用环境仍然需要较简捷的语言特性的支持,并推动了一股在ES5规范上发展的潜流,其中包括duktape、JerryScript、Espruino、mJS等。然而随着支持ES6的新引擎如雨后春笋般崛起,ES5的时代终归已经落幕,繁华不再。
主流引擎厂商开始通过ES6释放出它们的能量,于是JavaScript在许多新的环境中被应用起来,大量的新技术得以推动,例如,WebAssembly、Ohm、Deeplearn.js、TensorFlow.js、GPU.js、GraphQL、NativeScript等。有了Babel这类项目的强大助力,新规范得以“让少数人先用起来”,而标准的发布也一路披荆斩棘,以至于实现了“一年一更”。众多利好迅速反馈给前端,现在它们有了:
■ Chrome等浏览器中由内置高性能JavaScript引擎所释放出来的强大算力。
■ W3C+ECMAScript推动的Web环境全面标准化带来的统一且一致的技术蓝图。
■ Node.js等推动的前后端同构的开发语言和执行环境。
于是,之前被框架技术所引领的前端开始将注意力投放到本地渲染和工程化上。主流的技术方向从此开始进入工程化时代,带来了面向统一用户接触层的前端工程化、平台化和标准化等明显的跨代与跨界特征。试图将应用或产品逻辑前移的所谓“大前端”应运而生。至此,无论是在技术、工程还是组织等各个方面,前端都赢得了开创性的局面。
随着工程与项目规模的扩大,JavaScript语言的一项早期设计越来越成为一切发展的阻碍—它是一门弱类型、动态类型的语言。于是JavaScript不得不在推广过程中面临如何提供静态类型系统的问题,从而推动了TypeScript和Dart这类以ECMAScript转译器作为卖点的新语言的诞生。类似地,还有Facebook推出的Flow在静态类型检查方面的努力。而在这些语言和辅助工具的背后还有一股更为新生的力量,即打算对类型注解(Type annotations)做出规范的ECMAScript,它们被称为装饰器(Decorator)。
无论是面向语言自身的发展,还是应对语言应用环境的变化,它们貌似推动JavaScript行进在完全不同的道路上,但根本上却有着基本相同的限制、策略,以及目的。