为什么要使用Node.js?
在众多可用的Web应用程序开发平台中,有许多堆栈可供选择,为什么要选择Node.js,Node.js为什么能够脱颖而出?我们将在以下几节中获得这个问题的答案。
流行
Node.js正在迅速成为一个流行的开发平台,并被许多大小玩家采用。其中一个玩家是PayPal,他们正在使用Node.js编写的系统取代现有的基于Java的系统。其他大型Node.js采用者包括沃尔玛的在线电子商务平台以及LinkedIn和eBay。
有关PayPal的博客文章,访问:https://www.paypal-engineering.com/2013/11/22/node-js-at-pa ypal/。
据NodeSource统计,Node.js的用户正在迅速增长(有关更多信息,请访问https://nodesource.com/node-by-numbers)。这种增长的证据包括增加下载Node.js版本的带宽,新增的与Node.js相关的GitHub项目等等。
开发者虽然对JavaScript的兴趣非常浓厚,但多年来JavaScript一直处于停滞状态,从搜索量(GoogleInsights)及其作为编程技能的使用(Dice Skills Center)来衡量。对Node.js的兴趣一直在快速增长,但有迹象表明正在趋于稳定。
有关这方面的更多信息,请参阅https://itnext.io/choosing-typescript-vs-javascript-technology-popularity-ea978afd6b5f 或者http:// bit.ly/2q5cu0w。
最好不要随大流,因为有不同的人群,每个人都声称他们的软件平台很酷。Node.js虽然能做一些很酷的事情,但更重要的是平台的技术优势
无处不在的JavaScript
在服务器和客户机上使用相同的编程语言一直是Web届的一个长期梦想。这个梦想可以追溯到Java的早期,浏览器中的Java小程序是用Java编写的前端服务器应用程序,JavaScript最初被设想为和这些小程序类似的轻量级脚本语言。Java从来没有实现过它作为客户端编程语言的宣言,甚至“Java小程序”这个短语也正在被逐渐抛弃,成为客户端应用程序模型中的模糊记忆。我们最终以JavaScript作为浏览器客户端语言,而不是Java。通常,前端JavaScript开发者与服务器端团队所处的语言环境不同,后者可能使用PHP、Java、Ruby或Python进行开发。
随着时间的推移,浏览器中的JavaScript引擎变得异常强大,让我们能够编写更加复杂的浏览器端应用程序。有了Node.js,我们终于可以通过在Web浏览器和服务器上使用相同的编程语言——JavaScript实现Web应用程序的开发。
前端和后端使用统一语言具有以下几个潜在好处:
- 一个开发团队可以同时负责前端和后端开发工作。
- 可以更容易地在服务器和客户端之间迁移代码。
- 服务器和客户端之间的通用数据格式(JSON)。
- 服务器和客户端都有通用的软件工具。
- 用于服务器和客户端的通用测试或质量报告工具。
- 在编写Web应用程序时,视图模板可以在两端使用。
JavaScript语言非常流行,因为它在Web浏览器中无处不在。与其他语言相比,JavaScript是一种现代的高级的编程语言。由于它的流行,有大量经验丰富的JavaScript程序员人才。
利用谷歌对V8的支持
为了使Chrome成为一款流行且优秀的Web浏览器,谷歌将V8引擎投资打造成一款超高速的JavaScript引擎。因此,谷歌有巨大的动力继续改进V8。V8虽然是Chrome的JavaScript引擎,但也可以独立运行。
Node.js构建在V8 JavaScript引擎之上,能充分利用V8的所有特性。因此,Node.js能够快速实现JavaScript的新特性,因为这些新特性是由V8实现的,Node.js能够基于同样的原因获得同样的性能优势。
更精简、异步、事件驱动的模型
据称,Node.js体系结构构建在单个执行线程上,具有巧妙的面向事件的异步编程模型和快速的JavaScript引擎,比基于线程的体系结构能够节省大量的CPU资源和内存资源。其他使用线程进行并发的系统往往具有Node.js所没有的内存开销和复杂性。我们将在本章后面的部分进一步讨论这个问题。
微服务体系架构
微服务体系架构是软件开发中的一种新的架构概念。微服务专注于将大型Web应用程序拆分为可以由小型团队轻松开发的小型的、紧密关注的服务。虽然它们并不完全是一个新概念,但更多的是对旧的客户机-服务器计算模型的重构,微服务模式非常适合敏捷项目管理技术,并为我们提供了更精细的应用程序部署。Node.js是实现微服务的优秀平台。我们稍后再谈。
Node.js在经历重大分裂和敌对后变得更加强大
2014年和2015年期间,Node.js社区在政策、方向和控制方面面临重大分歧。io.js项目是一个敌对的分支,由一个想要整合几个特性并改变决策过程的团队驱动。最终的结果是Node.js和IO.JS知识库的合并,一个独立的Node.js基金会来运行这个项目,社区共同努力向前迈向共同的方向。
愈合这一裂痕的一个具体结果是迅速采用了新的ECMAScript语言特性。V8引擎正在迅速采用这些新功能,以推进Web开发的状态。反过来,Node.js团队正以V8中所展示的速度采用这些功能,这意味着Promise和异步函数正迅速成为Node.js程序员的现实。
归根结底,Node.js社区不仅在io.js分裂和后来的ayo.js分裂之后幸存了下来,而且社区和平台也因此变得更加强大。
在本节中,你了解了使用Node.js的几个原因。Node.js不仅是一个受欢迎的平台,背后有强大的社区支持,而且使用Node.js还有严重的技术原因。它的架构有一些关键的技术优势,所以让我们深入研究一下。
Node.js事件驱动体系架构
Node.js惊人的性能据说是因为它的异步事件驱动体系架枸和V8 JavaScript引擎的使用。这使Node.js能够同时处理多个任务,例如在来自多个Web浏览器的请求之间切换。Node.js的原始创建者Ryan Dahl遵循以下要点:
- 与依赖线程处理多个并发任务的应用程序服务器相比,单线程、事件驱动的编程模型更易于开发,复杂性和资源开销更低。
- 通过将阻塞函数调用转换为异步代码执行,您可以配置系统,以便在满足阻塞请求时发出事件。
- 你可以从Chrome浏览器中利用V8 JavaScript引擎,所有工作都将用于改进V8;因此,进入V8的所有性能增强都有利于Node.js。
在大多数应用服务器中,并发或处理多个并发请求是通过多线程体系架构实现的。在这样的系统中,任何数据请求或任何其他阻塞函数调用都会导致当前执行线程挂起并等待结果。处理并发请求需要有多个执行线程。当一个线程挂起时,另一个线程可以执行。当应用服务器启动和停止线程来处理请求时,这会导致混乱。每个挂起线程(通常等待输入/输出操作完成)都会消耗一个完整的调用堆栈内存,从而增加开销。线程增加了应用服务器的复杂性和服务器开销。
为了帮助我们理解为什么会这样,Node.js的创建者Ryan Dahl提供了以下示例。在2010年5月的Cinco de NodeJS演示中
(https://www.youtube.com/watch?v=M-sc73Y-zQA)Dahl问我们执行一行代码时会发生什么情况,如:
1 query('SELECT * from db.table', function (err, result) {
2 if (err) throw err; // handle errors operate on result
3 });
当然,当数据库层向数据库发送查询并等待结果或错误时,程序会在此时暂停。这是一个阻塞函数调用的示例。根据查询的不同,此暂停可能相当长(好吧,几毫秒,这在计算机时间中是很长的)。此暂停是错误的,因为执行线程在等待结果到达时无法执行任何操作。如果你的软件在运行单线程平台上,则整个服务器将被阻塞且无法响应。如果你的应用程序运行在基于线程的服务器平台上,则需要一个线程上 下文开关来响应到达的所有其他请求。到达服务器的未完的成连接数量越多,线程上下文切换的数量就越多。上下文切换不是免费的,因为每个线程状态需要更多的内存,CPU在线程管理开销上将花费更多的时间。
Node.js最初开发的关键灵感是单线程系统的简单性。单执行线程意味着服务器没有多线程系统的复杂性。这个选择意味着Node.js需要一个事件驱动模型来处理并发任务。代码不再等待阻塞请求的结果(例如从数据库检索数据),而是将事件分派给事件处理程序。
使用线程实现并发通常会存在很多问题,例如代价高昂且容易出错,Java的同步原语易出错,或者设计并发软件可能非常复杂且容易出错。复杂性来自对共享变量的访问以及避免线程之间死锁和竞争的各种策略。Java的同步原语就是这种策略的一个例子,显然许多程序员发现这种方式很难使用。有一种倾向是创建诸如java.util.concurrent之类的框架来减小线程并发的复杂性,但有些人认为,掩盖复杂性只会使问题变得更复杂。
Java程序员可能会在这一点上提出异议。也许他们的应用程序代码是针对Spring这样的框架编写的,或者他们直接使用JavaEE。在这两种情况下,他们的应用程序代码都不使用并发特性或处理线程,因此我们刚才描述的复杂性在哪里?仅仅因为复杂性隐藏在Spring和JavaEE中并不意味着没有复杂性和开销。
好的,我们明白了:虽然多线程系统可以做令人惊奇的事情,但它有其固有的复杂性。Node.js解决了什么?
Node.js解决了复杂性问题
Node.js要求我们以不同的方式思考并发性。从事件循环异步触发的回调是一种更简单的并发模型,更易于理解、更易于实现、更易于推理、更易于调试和维护。
Node.js有一个执行线程,无需等待I/O或上下文切换。相反,有一个事件循环,在事件发生时将事件分派给处理程序函数。本来会阻止执行线程的请求会异步执行,结果或错误会触发事件。任何阻塞或需要时间才能完成的操作都必须使用异步模型。
原始的Node.js范例将调度的事件传递给匿名函数。既然JavaScript具有异步函数,Node.js范式正在转
变,通过wait关键字处理的Promise来交付结果和错误。调用异步函数时,控件会快速传递到事件循环,而不会导致Node.js阻塞。事件循环继续处理各种事件,同时记录每个结果或错误的发送位置。
通过使用异步事件驱动的I/O,Node.js极大地节省了内存开销。
Ryan Dahl在Cinco de Node演示文稿中提出的一个观点是不同请求的执行时间层次结构。与硬盘上的对象或通过网络检索的对象(毫秒或秒)相比,内存中的对象访问速度更快(以纳秒为单位)。外部对象的较长访问时间是以无数个时钟周期来衡量的,当你的客户坐在他们的Web浏览器前准备继续浏览时,如果加载页面的时间超过两秒,那么客户就会很可能离开我们的页面。
因此,并发请求处理意味着使用一种策略来处理需要更长时间才能响应的请求。如果目标是避免多线程系统的复杂性,那么系统必须像Node.js那样使用异步操作。
这些异步函数调用是什么样子的?
Node.js中的异步请求
在Node.js中,我们前面查看的查询代码如下所示:
query('SELECT * from db.table', function (err, result) {
if (err) throw err; // handle errors operate on result
});
当结果(或错误)可用时,提供一个被调用的函数(因此称为回调函数)。查询函数仍然需要同样的时间,但不会阻塞执行线程,而是返回事件循环,然后可以自由地处理其他请求。Node.js最终将触发一个事件,调用此回调函数并显示结果或错误指示。
客户端JavaScript中使用了类似的范例,我们一直在编写事件处理程序函数。
JavaScript语言的进步为我们提供了新的选择。使ES2015 promise,等同代码如下:
1 query('SELECT * from db.table') .then(result => {
2 // operate on result
3 })
4 .catch(err => {
5 // handle errors
6 });
7 // handle errors
8 }
除了async和await关键字之外,这看起来像是我们用使其他语言编写的代码,而且更易于阅读。由于是使用await执行的,所以这仍然是异步执行代码。
这三段代码段都执行我们前面编写的查询。与其说查询是一个阻塞函数的调用,不如说是异步的不会阻
塞执行线程的异步代码。使用回调函数和promise(期约)的异步编码,Node.js有其自身的复杂性问题。通常,我们一个接一个地调用异步函数。对于回调函数,这意味着是深度嵌套的回调函数;对于Promise,这意味着一长串.then处理函数。除了代码的复杂性,我们还有错误和结果出现在不应该出现的地方。异步执行的回调函数跳到下一行代码上,而是被调用。执行顺序不是一行接一行,就像在同步编程语言中一样;相反,执行顺序由回调函数的执行顺序决定。
异步函数方法解决了这种编码复杂性。编码风格更自然,因为结果和错误都出现在该出现的地方。await关键字集成了异步结果处理,而不是阻塞执行线程。在async/await特性的封装下隐藏了很多细节,在本书中我们将广泛地介绍这个模型。
但是Node.js的异步体系结构真的提高性能了吗?
性能和利用率
Node.js的一些令人兴奋之处在于它的吞吐量(可以处理的每秒请求数)。根据类似应用程序的比较基准测试(例如,Apache)表明Node.js具有巨大的性能提升。
下面是一个简单的HTTP服务器(https://nodejs.org/en/),直接从内存返回Hello World消息:
1 var http = require('http');
2 http.createServer(function (req, res) {
3 res.writeHead(200, {'Content-Type': 'text/plain'});
4 res.end('Hello World\n');
5 }).listen(8124, "127.0.0.1");
6 console.log('Server running at http://127.0.0.1:8124/');
这是一个使用Node.js构建的更简单的Web服务器。http对象封装了http协议,其http.createServer方法创建了一个完整的Web服务器,在侦听方法中指定了侦听的端口。该Web服务器上的每个请求(无论是对任何URL的GET还是POST)都会调用提供的函数。它非常简单和轻便。在这种情况下,不管URL是什么,它都会返回一个简单的文本/纯文本,即Hello World响应。
Ryan Dahl在名为Ryan Dahl:Introduction to Node.js的视频中展示了一个简单的基准测试(在YouTube上的YUI Library频道上,https://www.youtube.com/watch? v=M-sc73Y-zQA)。它使用了与此类似的HTTP服务器,但返回了一个1兆字节的二进制缓冲区;Node.js给出了每秒822个请求的成绩,
而Nginx给出的成绩为每秒708个请求,比Nginx提高了15%。他还指出,Nginx的峰值内存为4兆字节,而Node.js的峰值内存为64兆字节。
关键的观察结果是,Node.js运行的是一种解释的、JIT编译的高级语言,其速度大约与Nginx一样快,Nginx由高度优化的C代码构建,同时运行类似的任务。该演示是在2010年5月,Node.js从那以后有了很大的改进,正如我们前面提到的Chris Bailey的演讲所示。
雅虎!搜索工程师Fabian Frank发布了一个使用Apache/PHP和Node.js堆栈的两个变体实现的真实搜索查询建议小部件的性能案例研究(http://www.slideshare.net/FabianFrankDe/nodejs-performance-case-
study). 该应用程序是一个弹出式面板,显示用户使用基于JSON的HTTP查询输入短语时的搜索建议。在相同的请求延迟下,Node.js版本每秒可以处理8倍的请求数。Fabian Frank说这两个Node.js堆栈都是线性扩展的,直到CPU使用率达到100%。
LinkedIn在服务器端使用Node.js对他们的移动应用程序进行了大规模的修改,以取代旧的Ruby on Rails应用程序。这迁移将他们的服务器从30向到3台,并且合并了前端和后端团队,因为所有内容都是用JavaScript编写的。在选择Node.js之前,他们使用Event Machine评估了Rails,使用Twisted评估了Python和和Node.js。他们选择Node.js,原因正是我们刚才讨论的。要了解LinkedIn具体的测试过程,请访问http://arstechnica.com/information-technology/2012/10/a-behind-the-scenes-look-atlinkedins-mobile-engineering/大多数现有的Node.js性能提示都是为使用了CrankShaft优化器的旧版V8编写的。V8团队已经完全抛弃了CrankShaft,并选择了新的优化器,叫做TurboFan例。例如,在CrankShaft下,使使用try/catch、let/const、generator等函数的速度较慢。因此,人们普遍认为不要使用这些特性,这很令人沮丧,因为我们想要使用新的JavaScript特性,因为新特性对JavaScript语言有很大的改进。Google V8团队的工程师Peter Marshall在Node.js Interactive 2017上发表演讲,声称使用TurboFan,你完全可以编写原生的JavaScript。使用TurboFan的目标是全面提升V8引擎的性能。要查看演示文稿,请访问https:// www.youtube.com/watch?v=YqOhBezMx1o。
关于JavaScript的一个“真理”是,由于JavaScript的性质,所以JvaScript不适合繁重的计算任务。我们将在下一节讨论与此相关的一些问题。Mikola Lysenko在Node.js Interactive 2016上的演讲讨论了JavaScript数值计算的一些问题,以及一些可能的解决方案。普通的数值计算涉及由数值算法处理的大型数值数组,这些数值算法可能是在微积分或线性代数课程中学习的。JavaScript缺少的是多维数组和对某些CPU指令的读取。JavaScriptt提出的解决方案是使用JavaScript多维数组库,以及数值计算算法库读
取CPU指令。要查看演示文稿,请参阅Mikola Lysenko在https上制作的名为“JavaScript中的数值计算”的视频:https://www.youtube.com/watch?v=1ORaKEzlnys。
在2017年的Node.js交互会议上,IBM的Chris Bailey证明Node.js是高度可扩展微服务的最佳选择。关键性能特征是I/O性能(以每秒事务数衡量)、启动时间(因为这限制了服务扩展到满足需求的速度)和内存占用(因为这决定了每台服务器可以部署多少应用程序实例)。Node.js在所有这些指标上都表现出色;在随后的每个版本中,它要么在每个度量上都有所改进,要么保持相当稳定。Bailey给出了一些数字,将Node.js与在Spring Boot中编写的类似基准进行了比较,显示Node.js的性能要好得多。要查看他的演讲,请参阅名为Node.js Performance and Highly Scalable Micro Services-Chris Bailey,IBM,https://www. youtube.com/watch?v=Fbhhc4jtGW4。
底线是Node.js在事件驱动的I/O吞吐量方面表现出色。Node.js程序能否在计算程序方面表现出色,取决于你能否巧妙地克服JavaScript语言中的一些缺陷。
计算式编程的一个大问题是阻止了事件循环的执行。正如我们将在下一节中看到的那样,这可能会使Node.js看起来不适合所有应用。
Node.js是一场恶性的可伸缩性灾难吗
2011年10月,一篇名为Node.js的博文(从发布该博文的博客中摘取)是一种称为Node.js的癌症——可伸缩性灾难。为证明这一点而展示的示例是斐波那契序列算法的CPU限制实现。虽然这个论点是有缺陷的,因为没有人实现斐波那契,这就证明了Node.js应用程序开发者必须考虑以下问题:在哪里进行繁重的计算任务?维持Node.js应用程序高吞吐量的关键是确保事件得到快速处理。因为它使用单个执行线程,所以如果该线程因大量计算而堵塞,Node.js将无法处理事件,事件吞吐量将受到影响。
斐波那契序列作为繁重计算任务的代名词,对于这样一个大大计算量的实现,计算成本很快就会变的高昂:
1 const fibonacci = exports.fibonacci = function(n) {
2 if (n === 1 || n === 2) {
3 return 1;
4 } else {
5 return fibonacci(n-1) + fibonacci(n-2);
6 } 7 }
这是一种计算斐波那契数的特别简单的方法。是的,有很多方法可以更快地计算斐波那契数列。我们将这个计算方法作为一个通用示例来演示当事件处理程序运行缓慢时Node.js会出现什么问题,而不是讨论计算数学函数的最佳方法。考虑以下服务器:
1 const http = require('http');
2 const url = require('url');
3 http.createServer(function (req, res) {
4 const urlP = url.parse(req.url, true);
5 let fibo;
6 res.writeHead(200, {'Content-Type': 'text/plain'});
7 if (urlP.query['n']) {
8 fibo = fibonacci(urlP.query['n']); // Blocking
9 res.end('Fibonacci '+ urlP.query['n'] +'='+ fibo); } else {
10 res.end('USAGE: http://127.0.0.1:8124?n=## where ##
11 is the Fibonacci number desired');
12 }
13 }).listen(8124, '127.0.0.1');
14 console.log('Server running at http://127.0.0.1:8124');
这是前面显示的简单Web服务器的扩展。Web服务器查找请求URL中的参数n,计算它的斐波那契数。计算结束后,结果将返回给调用者。
对于足够大的n值(例如,40),服务器将完全无响应,因为事件循环未运行。相反,计算函数已阻止事件处理,因为当函数在进行计算时,事件循环无法分派事件。换句话说,斐波那契函数是所有阻塞操作的替代品。
这是否意味着Node.js是一个有缺陷的平台?不,这只是意味着程序员必须注意识别长时间运行的计算代码并开发解决方案。包括重写处理事件循环的算法,通过重写算法提高效率,集成原生代码库,或者使用后端服务器计算需要大量计算性能的计算任务。
简单地重写通过事件循环分派的计算任务,让服务器继续处理事件循环中的请求。使用回调和闭包(匿名函数),使我们能够维护异步I/O和并发Promise,如以下代码所示:
1 const fibonacciAsync = function(n, done) {
2 if (n === 0) {
3 return 0;
4 } else if (n === 1 || n === 2) {
5 done(1);
6 } else if (n === 3) {
7 return 2;
8 } else {
9 process.nextTick(function() {
10 fibonacciAsync(n-1, function(val1) {
11 process.nextTick(function() {
12 fibonacciAsync(n-2, function(val2) { done(val1+val2); }); 13 });
14 });
15 });
16 }17 }
这是一种计算斐波那契数的同样愚蠢的方法,但是通过使用process.nextTick,事件循环有机会执行。由于这是一个采用回调函数的异步函数,因此需要对服务器进行一些重构:
1 const http = require('http');
2 const url = require('url');
3 http.createServer(function (req, res) {
4 let urlP = url.parse(req.url, true);
5 res.writeHead(200, {'Content-Type': 'text/plain'});
6 if (urlP.query['n']) {
7 fibonacciAsync(urlP.query['n'], fibo => { // Asynchronous
8 res.end('Fibonacci '+ urlP.query['n'] +'='+ fibo); 9 });
10 } else {
11 res.end('USAGE: http://127.0.0.1:8124?n=## where ## is the
12 Fibonacci number desired');
13 }
14 }).listen(8124, '127.0.0.1');
15 console.log('Server running at http://127.0.0.1:8124');
我们添加了一个回调函数来接收请求结果。在这种情况下,服务器能够处理多个斐波那契数列请求。但由于算法效率低下,仍然存在性能问题。在本书的后面,我们将更深入地探讨这个示例,以探索其他方法。同时,我们可以讨论为什么使用高效的软件堆栈很重要。
服务器利用率、间接成本和环境影响
追求最佳效率(每秒处理更多请求)不仅仅是极客对优化的孜孜不倦追求,还有涉及很多的商业效益和环境效益。就像Node.js服务器那样,每秒处理更多的请求意味着就可减少数服务器的采购数量。Node.js能够让你的公司用更少的成本处理更多的任务。
简单地说,你购买的服务器越多,货币成本和运营成本就越高。虽然在降低运行的Web服务器设施成本和环境影响方面,有专业的服务器供应商给你提供解决方案。但是很多时候这些服器供应商提供的方案并没有起到应有的作用。很明显,我们的目标是通过使用更高效的软件来减少服务器数量、降低成本和降低环境影响。
英特尔的论文,通过服务器功率测量提高数据中心效率 (https://www.intel.com/content/dam/doc/white-paper/intel-it-data-center-efficiency-server-power-paper.pdf) 给我们理解效率和数据中心成本提供了一个客观的框架。影响服务器效率和数计中心成本的因素有很多,例如建筑物、冷却系统和计算机系统设计。高效的建筑设计、高效的冷却系统和高效的计算机系统(数据中心效率、数据中心密度和存储密度)可以降低成本和环境影响。但相比高效的软件栈,部署低效的软件栈也会让你购买更多的服务器。或者你可以使用高效的软件堆栈来提升数据中心的效率,从而减少所需要的服务器数量。
关于高效软件栈的讨论不仅仅是为了保护环境,当然这是其中一个原因。但是绿色环保可以给你的业务底线带来更多的帮肋。
在本节中,我们了解了Node.js体系结构与其他编程平台的许多不同之处。Node.js选择避开线程来实现并发,并简化了使用线程带来的复杂性和额外的性能开销。这似乎实现了Node.js提高效率的Promise,效率对企业的许多方面都有好处。
拥抱JavaScript语言的进步
过去几年对于JavaScript程序员来说是一段激动人心的时光。负责ECMAScript标准的TC-39委员会给JavaScript增加了许多新特性,其中一些是语法糖,但同时其中一些新特性将我们推向了一个全新的JavaScript编程时代。async/await特性本身就为我们提供了一种摆脱所谓的callback Fall(回调地狱)的方法,即在回调中嵌套回调的问题。这是一个非常重要的特性,因此有必要对Node.js和JavaScript生态系统的其他部分中流行的面向回调的范例进行广泛的反思。
你在前面的页面中看到了以下内容:
1 query('SELECT * from db.table', function (err, result) {
2 if (err) throw err; // handle errors
3 // operate on result
4 });
这是Ryan Dahl的一个重要见解,也是推动Node.js流行的原因。某些操作需要很长时间才能执行,例如数据库查询,不应将数据库查询视为从内存中快速检索数据的操作。由于JavaScript语言的性质,Node.js不得不以一种不自然的方式表达这种异步编码结构。结果不会显示在下一行代码中,而是显示在这个回调函数中。此外,必须在回调函数中以非自然的方式处理错误。
在Node.js中,回调函数的第一个参数是错误指示器,后面的参数是结果。这是一个有用的约定,你可以在Node.js环境中找到;然而,这会使处理结果和错误变得复杂,因为二者都位于回调函数中。错误和结果自然会出现在代码的后续行上。
我们进一步深入回调地狱,每一层回调函数都嵌套在一起。第七层回调嵌套比第六层回调嵌套更复杂。为什么?如果没有其他原因的话,那是因为随着回调嵌套得越来越深,错误处理的特殊考虑变得越来越复杂。
但正如我们前面看到的,这是在Node.js中编写异步代码的新的首选方式:
1 const results = await query('SELECT * from db.table');
相反,ES2017异步函数让我们回到编程意图的这种非常自然的表达。结果和错误落在正确的位置,同时
保留了使Node.js伟大的优秀事件驱动异步编程模型。我们将在本书的后面部分看到这是如何工作的。
TC-39委员会向JavaScript添加了更多新功能,例如:
- 改进的类声明语法,使对象继承和getter/setter函数非常自然。
- 跨浏览器和Node.js标准化的新模块格式。字符串新方法,例如模板字符串表示法。
- 集合和数组的新方法,例如map/reduce/filter操作。
- const关键字用于定义无法更改的变量,let关键字定义变量,这些变量的作用域被限制在声明它们被的块中,而不是被提升在函数的前面。
- 新的循环结构和与这些新循环一起使用的迭代协议。
- 一种新的函数一箭头函数,更简洁,意味着更少的内存和执行时间。
- Promise对象表示Promise在将来提交的结果。Promise本身可以解决回调地狱问题,是构成了异步函数的重要基础。
- Generator函数是对一组值进行异步迭代的有趣方式。更重要的是,它们构成了异步函数的另一半基础。
你可能会看到新的JavaScript被称为ES6或者ES2015。那么目前最新的JavaScript版本的首选名称是什么?ES1到ES5标志着JavaScript开发的各个阶段。ES5于2009年发布,并在现代浏览器中广泛实现。从ES6开
始,TC-39委员会决定改变命名惯例,因为他们打算每年都增加新的语言特性。因此,语言版本名称现在包括年份,例如,2015年发布了ES2015,2016年发布了ES2016,2017年发布了ES2017。
部署ES2015/2016/2017/2018 JavaScript代码
由于受到用户使用的Web浏览器版本限制以及操作系统多年未更新的机器上使用的大量旧浏览器限制,前端JavaScript开发人员开发者往往无法使用JavaScript最新的功能。幸运的是,IE6已经几乎完全退役,但仍然有很多老旧的计算机安装有lE流浏览器。如果我们想让代码在IE浏览器上正常运行,着意味着我们需要将我们的代码转换成IE浏览器支持的JavaScript代码,即ES5代码。
Babel和其他代码重写工具就是用于处理这个问题的。虽然我们很多的产品用户都在使用IE浏览器。但是开发人员仍然可以使用最新的JavaScript或TypeScript功能编写代码,然后使用Babel将代码转换为IE浏览器能够运行的代码。通过这种方式,前端JavaScript开发人员可以使用JavaScript新特性,但代价是需要构建更加复杂的构建工具链,并尽量避免代码重写过程中引入错误的风险。
Node.js世界没有这个问题。Node.js能很快实现ES2015/2016/2017功能,就像在V8引擎中实现一样。从Node.js 8开始,我们可以自由将异步函数作为原生功能使用。Node.js版本10首先支持新的模块格式。
换句话说,虽然前端JavaScript程序员在使用ES2015/2016/2017功能之前必须等待几年,但Node.js程序员不需要等待。我们可以直接使用新功能,而不需要任何代码重构工具,除非我们的产品经理坚持要在不支持JavaScript新特性的低版本Node.js上发布的我们产品。在这种情况下,建议你使用Babel。
JavaScript领域的一些进步正在TC-39社区之外发生。
使用Node.js开发微服务或Maxiservice(超服务)
云部署系统和Docker等容器的出现使实现一种新的服务体系架构成为可能。Docker使得在一个可重复的容器中定义服务器进程配置成为可能,这个容器可以方便地部署到云托管系统中。这种容器最适合于小型、单一用途的服务实例,这些实例可以连接在一起构成一个完整的系统。Docker简化云部署的唯一工具;然而,Docker的特性非常适合现代应用程序的部署需求。
一些开发者使用微服务概念来描述此类系统加以推广。根据microservices.io网站的定义,微服务由一组列聚专有功能、可独立部署的服务器组成。通过将微服务与单一应用程序部署模式进行对比,在单一应用程序部署模式中,系统的每个方面都集成到一个捆绑包中(例如JavaEE AppServer的单个WAR文件)。微服务模型为开发人员提供了急需的灵活性。
微服务的一些优点如下:
- 每个微服务都可以由一个小团队管理。
- 只要保持服务器API的兼容性,每个团队都可以按照自己的时间表工作。
- 如果需要,可以独立部署微服务,例如为了更方便的测试。
- 切换技术堆栈选项更容易。
Node.js哪些方面微服务架构相适应?Node.js简直是为微服务模型架构量身定制的:
- Node.js鼓励小型、重点突出、单一用途的模块。
- 这些模块由优秀的npm软件包管理系统组成一个应用程序。
- 发布模块非常简单,无论是通过NPM存储库还是通过Git URL。
- 虽然像Express这样的应用程序框架可以用于大型服务,但它对于小型轻量级服务非常有效,并且支持轻松、简单的部署。
简而言之,以精益和敏捷的方式使用Node.js很容易,根据你的体系结构偏好构建大型或小型服务。
总结
总结
你在这一章了解了很多内容。具体地说,你看到了JavaScript在Web浏览器之外的生命力,Node.js是一个具有许多有趣属性的优秀编程平台。虽然Node.js是一个相对年轻的项目,但它已经变得非常流行,并且不仅广泛用于Web应用程序,还广泛用于命令行开发工具等。由于Node.js平台基于Chrome的V8 JavaScript引擎,因此项目能够跟上JavaScript语言的迭代速度。
Node.js体系结构使用由触发回调函数的事件循环管理的异步函数,而不是使用线程和阻塞I/O。该体系结构声称具有性能优势,似乎提供了许多优势,包括用更少的硬件完成更多工作的能力。但我们也了解到,效率低下的算法可能会抹去所有性能优势。
本书的重点是开发和部署Node.js应用程序的实际注意事项。我们将尽可能多地介绍Node.js应用程序的开发、优化、测试和部署。
现在我们已经对Node.js有做大致了解,我们准备开始深入研究并开始使用它。在第2章“设置Node.js”中,我们将介绍如何在Mac、Linux或Windows上设置Node.js开发环境,甚至编写一些代码。让我们开始吧。