文/田永强
唯有明晰历史,才能了然当下,预知未来。作者从历史角度解读Node.js,帮助读者透过猜忌和谣言,看清真实的Node.js,了解Node.js的核心与红利。
令人惴惴不安的Node.js
我们越来越频繁地看到关于JavaScript的新闻,刚开始谈到的是引擎性能提升,后来则是由HTML5和Node.js带来令人叹为观止的应用。如果只看表面,容易让人产生以下各种误解。
- Node.js的出现是让前端工程师羞辱后端吗?
- Node.js肯定是几个前端工程师在实验室里捣鼓出来的。
- 为了后端而后端,有意思吗?
- 异步是反人类的。
- 怎么又发明了一门新语言?
- JavaScript承担的责任太重了。
- 直觉上JavaScript不应该运行在后端。
JavaScript的诞生
JavaScript 1995年,诞生于网景公司,因为当时需要一种脚本语言协助Navigator做简单的操作。Brendan Eich受命操刀实现它。当年网景公司与Sun合作紧密,不懂技术的高管希望得到一个类似Java的脚本语言,期望它能像Java一样风靡。而 Brendan Eich原本期望进入网景公司做Scheme研究,却接到了一个他不喜欢的任务。形势所逼,他在10天的时间里完成这门语言的设计。它唯一符合高管们期望 的地方就是它长得确实像Java。
这看起来这本是一件糟糕的事,所幸Brendan Eich的才华确实令人高山仰止。高管期望的Java+Script实际上是C+Scheme+Self+Java的产物。它本不该叫 JavaScript,却获得了这个名字,谈起这件事,许多开发者会拿“雷锋”与“雷峰塔”的关系开玩笑。
JavaSript诞生后另一件糟糕的事出现在第一次浏览器大战时。微软在IE中实现了JScript,与JavaScript十分相似。网景为了保护自己,过早推动了这门语言标准化的进程,在1997年ECMAScript262规范颁布。
两件事情叠加在一起,使得JavaScript还没有足够的经历就被过早地出生和定型。它就像一个早产儿,之后的成长中,处处透露出不成熟的气息。 所幸福祸相依,看起来糟糕的事情背后,依然留下了两个拐点,为JavaScript保留了优秀的基因。
- 引入了Scheme的函数式,函数成为一等公民。函数式编程里的高阶函数、偏函数都充满了灵活性,它们在JavaScript中得以发挥。
- 引入了Self的原型链。得要感谢Brendan Eich的这个决定,不然现在大多数人可能只知道Java的模版继承,却不知道另一种继承方式叫做“原型链继承”。
最早的服务端JavaScript
很多人觉得JavaScript不适合运行在服务端,但这是错的,尽管一直处在Java的阴影下,但网景还是期望JavaScript能够既运行在浏览器 中,也能运行在服务器端。网景最早的服务端JavaScript项目叫做LiveWire,在今天的互联网上仍能看到它的遗迹 。当JavaScript运行在服务端时,PHP还没有盛行(PHP 最早版本于1995年发布)。
服务端JavaScript之殇
但理想与现实之间总是存在差距。浏览器市场份额之争使得JavaScript在浏览器中固化且无可替代。另一厢,Java风行互联网,服务端JavaScript完全没有抢眼球的机会。并且随着网景在浏览器战争的失利,后端JavaScript从此式微。
服务端JavaScript遗民
尽 管最初这条道路十分崎岖,甚至荒芜,但行人总是有的。因为这是一门图灵完备的语言。Rhino作为Java实现的JavaScript引擎一直存在。前端 JavaScript也一直作为表单校验功能为Web 1.0发光发热。这一切都让人觉得JavaScript只是一门辅助语言。这也是造成部分人误解它的原因,认为JavaScript的能力仅限于此,在看 似职责分明的架构体系中,扮演着基本角色。后端JavaScript尽管存在,但极少有应用场景。
第二次浏览器大战
直到2006年,JavaScript还处在打酱油的位置。但Web 2.0和第二次浏览器大战的到来,催生了翻天覆地的变化。Web开始趋向应用化,JavaScript的角色一下子变得重要起来,从小脚本开始出现大规模编程需求。
另 一个变化则是JavaScript引擎在V8的引领下,速度大幅提升。新闻中时常出现“提升x倍”这样的词。这带来了一个以往难以想象的事实——在脚本语 言中,JavaScript的速度已在顶尖之列。我的同事苏千提供了斐波那契数列的测试结果,这是一个纯考验CPU的运算:
为了让比较更为直观,加入了C和Go两个静态语言作为参考(如表1所示)。
表1 JavaScript和其他语言性能对比
可以看到,JavaScript在V8平台的表现与Luajit相差无几。借助C++模块,性能可以更进一步提升。如果需要了解最新的对比结果,可以访问网页。
2006-2009年,JavaScript得到了进一步发展,效果主要体现在两个方面:
- JavaScript引擎性能继续大幅提升;
- 趋向应用化,出现大规模编程的需求。
由于ECMAScript过早标准化,时至今日,JavaScript标准的核心其实只是其完整语言所涉及的极小部分,与DOM相关的API部分则被归于W3C规范。
CommonJS
沉寂许久之后,服务端JavaScript开始有所复苏。但那时JavaScript的表现还严重受到浏览器的制约,后端JavaScript也并没有标准化,呈现出一种混乱分离的状态。这时CommonJS出现了。
CommonJS基于语言现状和一直想要充分发挥JavaScript能力的愿望,开始制定JavaScript超出ECMAScript262、浏览器DOM与BOM API之外的规范。这些规范包括模块、包、二进制、流、Buffer、网络等。
这 是一个理想主义的做法,但直到Node.js出现,很少听闻它的消息。原因还在于人们的成见,认为JavaScript除了运行在浏览器中,没有其他作 为。它致力于将JavaScript作为命令行工具、桌面应用、服务甚至生态系统的理想,尽管在少部分人的帮助下得以实现,但在推广中收效甚微。
理想很美好,现实很骨感,但这还是埋下了又一伏笔,对于未来的发展至关重要:
CommonJS规范的出现可以指导JavaScript实现大规模编程。
这套规范后来不仅影响到了服务端JavaScript,也影响到前端JavaScript。
Node.js的逆袭
Node.js的出现终于让开发者开始关注服务端JavaScript,不过Node.js的作者并不是前端工程师,按时髦话来说,Ryan Dahl是一枚资深C/C++码农。
他 没有帮助前端工程师羞辱后端的愿望,也没有这份耐心。选择JavaScript,属于偶然中的必然。Ryan Dahl最初利用Ruby写了一个叫做Ebb的Web服务器。像其他资深C/C++码农一样,他将心思花费在服务器的实现上,例如放弃原来的Apache fork/prefork、多线程等模式,因为大家都意识到单线程异步的方式比原来的多线程或多进程具备更高的性能,Nginx就是这样的产物。此 外,Ryan Dahl还意识到,尽管面向客户端连接可以通过异步来提升性能,但在业务层面的一个同步I/O常让他在提升性能上所做的努力徒劳无功,因此他要的是面向客 户端的异步,面向后端业务,也是异步的一个服务器。
他开始评估几门语言实现这个Web服务器的可能——Ruby因为VM的性能原因被舍弃 了;尽管Ryan Dahl擅长C/C++,但他深知开发C/C++的门槛十分高。排除几种方案后,Lua是最接近他期望的语言。但Lua已有的I/O库是同步的,他知道, 如果在Lua中推行异步I/O,将会因为不符合人们的习惯而被抛弃。
这时JavaScript出现在了他的面前。也许是历史的必然(或巧 合),前文描述的伏笔或多或少影响了Ryan Dahl。JavaScript的单线程、事件驱动的方式也十分贴合Ryan Dahl的理念。更重要的是,异步在JavaScript中已广泛使用(例如Ajax),服务端JavaScript的停滞给Ryan Dahl留出了巨大空白——于是Node.js诞生了。
Node.js的出现给服务端JavaScript带来了新气象,CommonJS也应用到了Node.js的模块和包上。凭借V8和异步I/O带来的性能优势,一扫曾经的阴霾。尤其是将异步引入到业务层,非阻塞特性使得JavaScript的语言模型显得足够独特。
Node.js 与Nginx之间也可以稍作比较。通过我们的线下测试,发现Nginx的性能比Node.js的HTTP模块要好很多。但Nginx考量的是面向客户端, 后端业务方面依然是受具体业务影响。而Node.js则可以利用异步I/O来实现业务并行,以提升效率。从这点看,Nginx没有Node.js灵活。此 外,Node.js后来的发展方向不再单独是一个Web服务器,而是一个面向网络的平台,它甚至可以是TCP服务器,或者变身为远端服务器的客户端。
异步的利与弊
异 步带来的好处主要是性能方面,如果没有异步,就没有高性能服务器。异步让CPU与I/O之间重叠利用,可以很好地利用硬件,提高吞吐率。异步早就应用在操 作系统层面中,在操作系统内部,处处是信号量,只是在业务层面出现较少。将Node.js广泛应用异步到业务层,在面向网络时的效果是明显的。
为了直观地展示在网络中应用异步I/O的重要性,下面这组数据必不可少:
表2 网络中的I/O开销
我给出的题目很简单:如果在分布式环境中,需要从不同的地方调用10次不同的数据,时间消耗为t1到t10不等,总计消耗时间该为多少?如果没有异步I/O,同步方式将会花费很多时间,而理想状况下的异步调用则为max(t1,…,t10)。
异 步令人不满的地方在于流程控制会带来麻烦。但以日常生活为例,生活中处处是异步:排队到窗口买饭是同步,坐在桌子上点菜则是异步;网购是异步,自己跑去超 市买东西拎回家则是同步。我们可以发现生活中的异步并未给我们带来多大烦恼,相反,同步的痛楚可能更明显。但在代码中,却又是相反的。不过好在问题总是可 解的:将逻辑分发,或者采用赵劼开发的Wind.js都是可选方案。
拥抱开源与生态圈成型
随 着Node.js带来服务端JavaScript的进步,如今JavaScript的生态圈已经形成。这里有个有趣的现象,CommonJS立下的志愿最 初没有被实现,但随着Node.js采用CommonJS的部分规范,Node.js具备了理论上的指导。模块、包规范的应用,使得Node.js不像最 初浏览器中的JavaScript一样无序混乱。Node.js在应用过程中催生了NPM工具,而NPM工具的广泛应用则起到了推广CommonJS规范 的效果。这是一个种因得果的循环,有理想,则最终有其实现。
另一件类似的现象体现在V8与Node.js的关系上,Node.js因为V8的优异性能,选择以它作为基础构建平台,但随着Node.js不同于浏览器的应用场景,又反过来促进V8的性能提升。
Node.js 自身开源,同时也推动开源,NPM上的模块数量如今已超过14000,日下载量接近50万次,社区的活跃程度史无前例。尽管我们知道Node.js继承了 一些JavaScript带来的缺点,但在如此强大的社区面前,在一个解决方案比问题还多的社区面前,想没有信心都难。
作为一家企业,我们在淘宝内部也搭建了NPM的镜像服务器,充分利用NPM来管理内部私有模块,减少拷贝粘贴行为,将一切可以模块化的功能或方案尽皆做成包,共享给兄弟项目或者发布到官方NPM上,回馈给开源社区。
如 果一个开源社区的循环是良性的,我们没有理由不信任它,因为成型生态圈,使它具有持久的生命力。但有一件事必须明了,尽管这个生态圈已然成型,但 Node.js自身与现有技术并不是非此即彼的,发挥异步并行的优势,与其他系统异构共存,将会是很长一段时间内的主题。即便是理念最为相近的 Nginx,也是可以与Node.js互相协助的。
总结
也许是早产注定了JavaScript留给开发者发挥的余地够多,尽管不是有意缺少,但确实体现出了Less is More的精髓。按赵劼的话说,尽管JavaScript有很多毛病,但它设计得还不赖。相信后续的ECMAScript标准化能够修缮过去的问题。在经 历了多年的发展以后,JavaScript已不是从前的它,从核心上衍生出的红利也是显而易见的,生态圈及前后端语言打通,使得更多开发者可以参与其中。 随着Node.js在服务端大展拳脚,第二波JavaScript浪潮已经到来,如果依旧怀抱过去的观点,等着被恭喜Out吧。
作者田永强,花名朴灵,2011年在上海组织CNode社区的线下分享会。2012年加入淘宝的数据产品团队专职参与Node.js开发。