1.1 Node.js介绍

近年来,Node.js技术社区蓬勃发展,越来越多的人致力于把这项成果发扬光大,许多使用Node.js搭建的项目逐渐为人们所熟知,Node.js也成为JavaScript技术圈中的热门话题。从人们的讨论中读者也许已经获得了关于Node.js的一些感性认知。例如,Node.js拓宽了前端开发者的技术领域,成为从前端开发领域伸向服务端开发领域的一只触手;因为Node.js的出现,JavaScript从一门“玩具语言”摇身一变成为能够满足工程开发需要的严谨的编程语言;等等。

然而,这个看起来妙不可言的东西到底是什么呢?Node.js有什么特点?它是怎样工作的?为什么需要Node.js?关于这些,Node.js官方网站给出的描述极其简洁:

Node.js是一个基于Chrome v8引擎的JavaScript运行环境。Node.js使用了一个事件驱动、非阻塞式I/O的模型,使其轻量又高效。Node.js的包管理器NPM,是全球最大的开源库系统。

1.1.1 什么是Node.js

Node.js是一个基于Chrome v8引擎的JavaScript运行时环境,其官方图标如图1.1所示。然而什么是运行时环境,它又为什么要基于Chrome v8引擎呢?这两个问题将有助于我们理解Node.js的基本定义。

图1.1 Node.js官方图标

运行时环境或运行时,更确切的称谓是Managed Runtime Environment,即托管运行时环境。JavaScript引擎则是对同一个概念的更通俗叫法。运行时是一个平台,它把运行在底层的操作系统和体系结构的特点抽象出来,承担了解释与编译、堆管理(Heap Management)、垃圾回收机制(Garbage Collection)、内存分配(Memory Allocation)、安全机制(Security)等功能。在这些运行时环境中开发应用的开发者可以不用关心底层的计算机处理器指令,而把更多的精力投入到更为关键的业务逻辑中去。

许多高级程序语言都带有配套的运行时环境,如Java和C++。这些运行时环境提供了以往由计算机处理器和操作系统所提供的功能,即为存在于各种各样的设备上的不同操作系统解释并运行由不同编程语言编写的应用。若没有这些运行时环境的介入,特定的操作系统所能识别的编程语言是极其有限的,因此能够在该操作系统上运行的应用也将非常有限。运行时环境使开发者能够以成本最小的方式创建应用。

由于运行时环境和操作系统及计算机的体系结构有着密切的联系,因此它常常被称为虚拟机(Virtual Machine,即VM)。在JavaScript的开发语境下,因为缺少指令集,所以Machine的概念被弱化了。但不管是虚拟机、引擎,还是运行时环境,其实都被用来指代同一种东西:JavaScript的托管运行时环境。

简而言之,JavaScript运行时环境就是一个能够执行JavaScript语句的运行环境,它提供一系列以往由处理器和操作系统才能提供的功能,使得开发者能够脱离底层指令,从而专注于业务逻辑开发。

在Node.js出现以前,JavaScript主要运行在浏览器环境中,这是因为只有浏览器才具有能够解释JavaScript的机制,而Node.js使得JavaScript突破了浏览器的限制,开启了JavaScript的后端开发之路。

Chrome v8引擎是一个高性能的JavaScript解释引擎。Chrome浏览器内核是鼎鼎大名的WebKit的一个分支(WebKit分为渲染引擎WebCore和JavaScript解释引擎JavaScriptCore两部分)。Google认为运行现代Web应用需要一个强劲的JavaScript引擎,然而JavaScriptCore的运行效率并不让人满意。于是Google开发了一个高性能的JavaScript引擎,这个引擎就是Chrome v8。

因此,基于Chrome v8引擎的Node.js是一个能够轻而易举编写高性能Web服务的运行时环境。

1.1.2 Node.js的历史和发展过程

罗马并非一日建成的,Node.js作为高性能Web服务器也不是从某个天才的脑瓜中突然冒出来的。

Node.js的创始人Ryan Dahl并非科班出身——当年在罗彻斯特大学数学系学习的Ryan Dahl,因为讨厌抽象的代数拓扑学课程而放弃攻读博士学位,到南美旅行并成为一名使用Ruby on Rails的Web开发者。虽然Ryan Dahl并非科班出身,但在大多数人看来Ryan Dahl无疑是个计算机技术方面的天才:两年内通过接各种应用开发工作到各地工作、旅行,Ryan Dahl最后成了专门为客户解决Web服务器性能问题的专家。Node.js并不是Ryan Dahl在解决Web服务器高并发问题方面的第一次尝试。在此之前,Ryan Dahl使用过Ruby、C和Lua,但均以失败告终。经过一系列摸索,Ryan觉得解决问题的关键是非阻塞和异步I/O。

就在这个时候,Chrome发布了高性能的v8引擎。Ryan Dahl仔细地分析以后发现这是一个绝佳的JavaScript运行环境,并且毫无疑问是他一直在寻找的东西,因为它不但是单线程的,而且已经实现了非阻塞。这使Ryan Dahl异常兴奋(图1.2为Ryan Dahl在接受采访)。

图1.2 Ryan Dahl在接受采访

在几个月的时间内,Ryan独自工作并完成了开发Node.js的第一步。在2009年底的JSConf EU会议上他发表了关于Node.js的演讲,模块管理工具NPM也在次年初被引入,用以简化Node.js模块源代码的发布、分享、安装及更新等流程。在接下来的三四年时间,他把自己的精力大量地倾注到Node.js的开发和发展中去,加入Joyent公司并成了Node.js的第一任“Gatekeeper”。

从那时起,Node.js从个人项目变成了公司组织下的项目。在Joyent公司的积极推动下,Node.js蓬勃发展,历任“Gatekeeper”——Ryan Dahl、Isaac Z. Schlueter、Timothy J Fontaine都是Node.js的重要贡献者,并且都是Joyent的全职员工。Joyent还在2011年协同微软一起发布了Windows版本的Node.js。可以说,Joyent对于Node.js早期的健康发展功不可没。然而在2014年前后,由于社区需求和公司需求的冲突,导致Node.js的主要贡献者企图脱离原本由Joyent所维护和赞助的Node.js体系,并从Node.js中分出一个分支,命名为io.js。2015年初,io.js发布了1.0.0版本。从那时起,Node.js的版本更迭实际上转移到了io.js上。因为脱离了公司体系,io.js不管是在管理模式还是行为上都与原本的Node.js大为不同。io.js对于新功能的态度更为激进,对Chrome 8引擎的新功能保持很快的跟进速度并在高频迭代下飞速发展。由于Node.js的主要贡献者都在io.js上工作,因此所有问题都能够得到很快的反馈。

对于社区的分裂,Joyent公司很快便做出了改进:成立了一个顾问委员会来打造一个更加开放的管理模式。委员会决定成立基金会并把Node.js迁移过去。在那之后的几个月,委员会一直在寻求同io.js的和解。2015年5月,为了项目自身的利益着想,io.js也加入了基金会,实际上成了Node.js的尝鲜版。也就是说,新功能都会首先在io.js上线,待稳定后再合并到Node.js中。由于io.js在与Node.js分离的几个月内版本更迭频繁,因此它的技术成熟度已经超前Node.js很多。在许多新的功能逐渐被添加到Node.js中之后,Node.js跳过了1.0版本,直接发布了2.0版本。

这里附上Node.js的版本更新时间轴,如图1.3所示。

图1.3 Node.js的版本更新时间轴

虽然在Node.js问世四年之后,Ryan转战Go,并坦言觉得Node.js并不是构建大型服务器网站的最佳选择,但Node.js无疑使JavaScript的开发者们意识到他们能够做到的事情更多了。这对JavaScript社区的发展来说具有里程碑式的意义。

备注:Ryan Dahl后来成为Google Brain见习项目的成员,在Google作为软件工程师从事机器学习相关的研究工作。

1.1.3 Node.js的特点和应用场景

Node.js使用了事件驱动、非阻塞式I/O模型,轻量又高效。

事件驱动是一种处理数据的方式,这种方式同传统的数据处理方式CRUD(增加、读取、更新、删除)截然不同。在CRUD模式中只保存数据的当前状态,因此所有的后续变更都会直接在数据本体上进行处理。这样做的弊端主要有:

· CRUD会直接在数据存储区进行操作,会降低响应速度和性能水平,对进程的开销过大也会限制项目的规模和可扩展性。

· 在存在大量并发用户的协作域中,对于同一数据主体的操作很可能会引起冲突。

· 在没有额外监听措施的情况下,任何节点能够获得的只有当前的状态快照,历史数据会丢失。

事件驱动(Event Sourcing)定义了一种由事件驱动的数据处理方式,应用发送的所有事件都会被载入附加存储区,每一个事件都代表了一系列的数据变更。被保留下来的事件会作为操作历史留存下来,与此同时事件流会被不间断地同步到客户端供其使用,例如更新整体的物化视图(Materialized View),把事件流提供给外部系统,或者通过重演与特定物体有关的历史事件来确定它的当前状态等。以在虚拟商城中添加物品到购物车的过程为例,事件驱动的数据处理过程如图1.4所示。

图1.4 事件驱动的数据处理过程

对比CRUD,事件驱动的优势是显而易见的:

· 已经发生的事件是不可更改的,并且只在附加区域中存储而不影响主线程,因此对事件进行处理的操作完全可以在后台进行而不影响到客户端的UI和内容展示。这对性能优化和提升应用的可扩展性来说大大有利。

· 不同用户对同一个对象的同时操作不会产生冲突,因为这种数据处理方式避免了对数据本身的直接更改。

· 附加区域中存储的事件流实际上提供了一个监听机制,使开发者能够通过重演历史事件的方式来获取当前状态,进而有助于系统的测试和漏洞修复。

提示:更多关于事件驱动和物化视图的内容请参考Microsoft Azure的官方文档,地址如下:https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing,https://docs.microsoft.com/en-us/azure/architecture/patterns/materialized-view。

事件驱动的异步I/O模型使得Node.js非常适合用来处理I/O密集型应用,但也不限于此,例如Web聊天室(Socket.io)、Web博客(Hexo)、Web论坛(Node Club)、前端模块管理平台(Bower.js)、浏览器环境工具(Browserify)、命令行工具(Commander)等。流行的Node.js应用框架有Express、Koa、Meteor等。

提示:Node.js能够运行在Linux、macOS、Microsoft Windows、UNIX等平台上,并且可以用能够被编译为JavaScript的任何语言(包括CoffeeScript和TypeScript等)进行编写。使用Node.js进行开发的过程其实就是使用JavaScript结合一系列处理核心功能的模块来创建如Web服务器、通信等工具的过程。其中,这些模块承担着诸如读取与存入文件、通信、双向数据交换、加密解密等功能,且开放API(Application Programming Interface,应用程序编程接口)供开发者使用。

1.1.4 安装Node.js

截至2018年5月,Node.js的稳定版本为8.11.1,最新版本为10.1.0,官网最新版本信息如图1.5所示。

图1.5 Node.js官网最新版本信息

中文官网地址为https://nodejs.org,下载最新版本安装文件并运行,将看到图1.6所示的Node.js安装界面。

图1.6 Node.js安装界面

提示:安装Node.js时会默认安装NPM(Node Package Manager),即Node.js模块管理工具。关于该部分内容请参看1.2节。

根据安装界面上的提示信息安装完毕后将看到如图1.7所示的界面,单击“关闭”按钮完成安装流程。

图1.7 Node.js安装完毕的界面

打开命令行工具输入指令检查是否安装成功,命令如下:

如果输出图1.8所示的Node.js版本信息,则表示安装成功。

图1.8 Node.js版本信息

1.1.5 实战演练:使用Node.js搭建一个HTTP Server

本示例将使用Node.js搭建一个用以展现本地图片的Web服务器,要求当使用浏览器打开地址http://localhost:8080/时,显示如图1.9所示的Logo。

图1.9 Logo

代码文件目录如图1.10所示。

图1.10 代码文件目录

images文件夹中存放了5张用于展示的Logo图片。index.html为页面的模板文件,里面包含了所需的HTML文本和相应的样式内容。index.js包含启动Node.js服务的核心代码,代码如下:

代码第10至12行,当用户访问根路径时,HTTP服务端将读取模板文件index.html,并返回响应。

代码第13至15行,当浏览器端解析完毕响应返回的HTML文档,接着会陆续下载5张Logo图片。浏览器发送的获取图片的请求将会执行该段逻辑,Node.js服务根据请求中的图片路径读取images文件夹中的图片文件内容并返回响应。