Node.js简介
第 1 章
2 第 1 章 Node.js 简介
Node.js,或者 Node,是一个可以让 JavaScript 运行在服务器端的平台。它可以让JavaScript 脱离浏览器的束缚运行在一般的服务器环境下,就像运行 Python、Perl、PHP、Ruby 程序一样。你可以用 Node.js 轻松地进行服务器端应用开发,Python、Perl、PHP、Ruby 能做的事情 Node.js 几乎都能做,而且可以做得更好。
Node.js 是一个为实时Web(Real-time Web)应用开发而诞生的平台,它从诞生之初就充分考虑了在实时响应、超大规模数据要求下架构的可扩展性。这使得它摒弃了传统平台依靠多线程来实现高并发的设计思路,而采用了单线程、异步式I/O、事件驱动式的程序设计模型。这些特性不仅带来了巨大的性能提升,还减少了多线程程序设计的复杂性,进而提高了开发效率。
Node.js 最初是由 Ryan Dahl 发起的开源项目,后来被 Joyent 公司注意到。Joyent 公司将Ryan Dahl 招入旗下,因此现在的 Node.js 由 Joyent 公司管理并维护。尽管它诞生的时间(2009年)还不长,但它的周围已经形成了一个庞大的生态系统。Node.js 有着强大而灵活的包管理器(node package manager,npm),目前已经有上万个第三方模块,其中有网站开发框架,有 MySQL、PostgreSQL、MongoDB 数据库接口,有模板语言解析、CSS 生成工具、邮件、加密、图形、调试支持,甚至还有图形用户界面和操作系统 API工具。由 VMware 公司建立的云计算平台 Cloud Foundry 率先支持了 Node.js。2011年6月,微软宣布与 Joyent 公司合作,将 Node.js 移植到 Windows,同时 Windows Azure 云计算平台也支持 Node.js。Node.js 目前还处在迅速发展阶段,相信在不久的未来它一定会成为流行的Web应用开发平台。让我们从现在开始,一同探索 Node.js 的美妙世界吧!
1.1 Node.js 是什么
Node.js 不是一种独立的语言,与 PHP、Python、Perl、Ruby 的“既是语言也是平台”不同。Node.js 也不是一个 JavaScript 框架,不同于 CakePHP、Django、Rails。Node.js 更不是浏览器端的库,不能与 jQuery、ExtJS 相提并论。Node.js 是一个让 JavaScript 运行在服务端的开发平台,它让 JavaScript 成为脚本语言世界的一等公民,在服务端堪与 PHP、Python、Perl、Ruby 平起平坐。Node.js 是一个划时代的技术,它在原有的 Web 前端和后端技术的基础上总结并提炼出了许多新的概念和方法,堪称是十多年来 Web 开发经验的集大成者。Node.js 可以作为服务器向用户提供服务,与 PHP、Python、Ruby on Rails 相比,它跳过了 Apache、Nginx 等 HTTP 服务器,直接面向前端开发。Node.js 的许多设计理念与经典架构(如 LAMP)有着很大的不同,可提供强大的伸缩能力,以适应21世纪10年代以后规模越来越庞大的互联网环境。
Node.js 与 JavaScript 说起 JavaScript,不得不让人想到浏览器。传统意义上,JavaScript 是由 ECMAScript、文档对象模型(DOM)和浏览器对象模型(BOM)组成的,而 Mozilla 则指出 JavaScript 由Core JavaScript 和 Client JavaScript 组成。之所以会有这种分歧,是因为 JavaScript 和浏览器之间复杂的历史渊源,以及其命途多舛的发展历程所共同造成的,我们会在后面详述。我们可以认为,Node.js 中所谓的 JavaScript 只是 Core JavaScript,或者说是 ECMAScript 的一个实现,不包含 DOM、BOM 或者 Client JavaScript。这是因为 Node.js 不运行在浏览器中,所以不需要使用浏览器中的许多特性。
Node.js 是一个让 JavaScript 运行在浏览器之外的平台。它实现了诸如文件系统、模块、包、操作系统 API、网络通信等 Core JavaScript 没有或者不完善的功能。历史上将 JavaScript 移植到浏览器外的计划不止一个,但Node.js 是最出色的一个。随着 Node.js 的成功,各种浏览器外的 JavaScript 实现逐步兴起,因此产生了 CommonJS 规范。CommonJS 试图拟定一套完整的 JavaScript 规范,以弥补普通应用程序所需的 API,譬如文件系统访问、命令行、模块管理、函数库集成等功能。CommonJS 制定者希望众多服务端 JavaScript 实现遵循CommonJS 规范,以便相互兼容和代码复用。Node.js 的部份实现遵循了CommonJS规范,但由于两者还都处于诞生之初的快速变化期,也会有不一致的地方。
Node.js 的 JavaScript 引擎是 V8,来自 Google Chrome 项目。V8 号称是目前世界上最快的 JavaScript 引擎,经历了数次引擎革命,它的 JIT(Just-in-time Compilation,即时编译)执行速度已经快到了接近本地代码的执行速度。Node.js 不运行在浏览器中,所以也就不存在 JavaScript 的浏览器兼容性问题,你可以放心地使用 JavaScript 语言的所有特性。
1.2 Node.js 能做什么
正如 JavaScript 为客户端而生,Node.js 为网络而生。Node.js 能做的远不止开发一个网站那么简单,使用 Node.js,你可以轻松地开发:
具有复杂逻辑的网站;
基于社交网络的大规模 Web 应用;
Web Socket 服务器;
TCP/UDP 套接字应用程序;
命令行工具;
交互式终端程序;
带有图形用户界面的本地应用程序;
单元测试工具;
客户端 JavaScript 编译器。
Node.js 内建了 HTTP 服务器支持,也就是说你可以轻而易举地实现一个网站和服务器
的组合。这和 PHP、Perl 不一样,因为在使用 PHP 的时候,必须先搭建一个 Apache 之类的
4 第 1 章 Node.js 简介
HTTP 服务器,然后通过 HTTP 服务器的模块加载或 CGI 调用,才能将 PHP 脚本的执行结果呈现给用户。而当你使用 Node.js 时,不用额外搭建一个 HTTP 服务器,因为 Node.js 本身就内建了一个。这个服务器不仅可以用来调试代码,而且它本身就可以部署到产品环境,它的性能足以满足要求。
Node.js 还可以部署到非网络应用的环境下,比如一个命令行工具。Node.js 还可以调用C/C++ 的代码,这样可以充分利用已有的诸多函数库,也可以将对性能要求非常高的部分用C/C++ 来实现。
1.3 异步式 I/O 与事件驱动
Node.js 最大的特点就是采用异步式 I/O 与事件驱动的架构设计。对于高并发的解决方案,传统的架构是多线程模型,也就是为每个业务逻辑提供一个系统线程,通过系统线程切换来弥补同步式 I/O 调用时的时间开销。Node.js 使用的是单线程模型,对于所有 I/O 都采用异步式的请求方式,避免了频繁的上下文切换。Node.js 在执行的过程中会维护一个事件队列,程序在执行时进入事件循环等待下一个事件到来,每个异步式 I/O 请求完成后会被推送到事件队列,等待程序进程进行处理。
例如,对于简单而常见的数据库查询操作,按照传统方式实现的代码如下:
res = db.query('SELECT * from some_table');
res.output();
以上代码在执行到第一行的时候,线程会阻塞,等待数据库返回查询结果,然后再继续处理。然而,由于数据库查询可能涉及磁盘读写和网络通信,其延时可能相当大(长达几个到几百毫秒,相比CPU的时钟差了好几个数量级),线程会在这里阻塞等待结果返回。对于高并发的访问,一方面线程长期阻塞等待,另一方面为了应付新请求而不断增加线程,因此会浪费大量系统资源,同时线程的增多也会占用大量的 CPU 时间来处理内存上下文切换,而且还容易遭受低速连接攻击。
看看Node.js是如何解决这个问题的:
db.query('SELECT * from some_table', function(res) {
res.output();
});
这段代码中 db.query 的第二个参数是一个函数,我们称为回调函数。进程在执行到db.query 的时候,不会等待结果返回,而是直接继续执行后面的语句,直到进入事件循环。当数据库查询结果返回时,会将事件发送到事件队列,等到线程进入事件循环以后,才会调用之前的回调函数继续执行后面的逻辑。Node.js 的异步机制是基于事件的,所有的磁盘 I/O、网络通信、数据库查询都以非阻塞的方式请求,返回的结果由事件循环来处理。图1-1 描述了这个机制。Node.js 进程在同一时刻只会处理一个事件,完成后立即进入事件循环检查并处理后面的事件。这样做的好处是,CPU 和内存在同一时间集中处理一件事,同时尽可能让耗时的 I/O 操作并行执行。对于低速连接攻击,Node.js 只是在事件队列中增加请求,等待操作系统的回应,因而不会有任何多线程开销,很大程度上可以提高 Web 应用的健壮性,防止恶意攻击。
这种异步事件模式的弊端也是显而易见的,因为它不符合开发者的常规线性思路,往往需要把一个完整的逻辑拆分为一个个事件,增加了开发和调试难度。针对这个问题,Node.js第三方模块提出了很多解决方案,我们会在第6章中详细讨论。
1.4 Node.js 的性能
1.4.1 Node.js 架构简介
Node.js 用异步式 I/O 和事件驱动代替多线程,带来了可观的性能提升。Node.js 除了使
用 V8 作为JavaScript引擎以外,还使用了高效的 libev 和 libeio 库支持事件驱动和异步式 I/O。
Node.js 的开发者在 libev 和 libeio 的基础上还抽象出了层 libuv。对于 POSIX①操作系统,libuv 通过封装 libev 和 libeio 来利用 epoll 或 kqueue。而在 Windows 下,libuv 使用了 Windows
——————————
① POSIX(Portable Operating System Interface)是一套操作系统 API 规范。一般而言,遵守 POSIX 规范的操作系统指的是 UNIX、Linux、Mac OS X 等。
6 第 1 章 Node.js 简介
的 IOCP(Input/Output Completion Port,输入输出完成端口)机制,以在不同平台下实现同样的高性能。
1.4.2 Node.js 与 PHP + Nginx
Snoopyxd 详细对比了 Node.js 与 PHP+Nginx 组合,结果显示在3000并发连接、30秒的
测试下,输出“hello world”请求:
PHP 每秒响应请求数为3624,平均每个请求响应时间为0.39秒;
Node.js 每秒响应请求数为7677,平均每个请求响应时间为0.13秒。
而同样的测试,对MySQL查询操作:
PHP 每秒响应请求数为1293,平均每个请求响应时间为0.82秒;
Node.js 每秒响应请求数为2999,平均每个请求响应时间为0.33秒。
关于 Node.js 的性能优化及生产部署,我们会在第6章详细讨论。
1.5 JavaScript 简史
作为 Node.js 的基础,JavaScript 是一个完全为网络而诞生的语言。在今天看来,JavaScript 具有其他诸多语言不具备的优势,例如速度快、开销小、容易学习等,但在一开始它却并不是这样。多年以来,JavaScript 因为其低效和兼容性差而广受诟病,一直是一个被人嘲笑的“丑小鸭”,它在成熟之前经历了无数困难和坎坷,个中究竟,还要从它的诞生讲起。
1.5.1 Netscape 与 LiveScript
JavaScript 首次出现在1995年,正如现在的 Node.js 一样,当年 JavaScript 的诞生决不是偶然的。在1992年,一个叫 Nombas 的公司开发了“C减减”(C minus minus,Cmm)语言,后来改名为 ScriptEase。ScriptEase 最初的设计是将一种微型脚本语言与一个叫做 Espresso Page 的工具配合,使脚本能够在浏览器中运行,因此 ScriptEase 成为了第一个客户端脚本语言。网景公司也想独立开发一种与 ScriptEase 相似的客户端脚本语言,Brendan Eich①接受了这一任务。起初这个语言的目标是为非专业的开发人员(如网站设计者),提供一个方便的工具。大多数网站设计者没有任何编程背景,因此这个语言应该尽可能简单、易学,最终一个弱类型的动态解释语言 LiveWire 就此诞生。LiveWire 没过多久就改名为 LiveScript 了,直到现在,在一些古老的 Web 页面中还能看到这个名字。
1.5.2 Java 与 Javascript
在JavaScript 诞生之前,Java applet②曾经被热炒。之前 Sun 公司一直在不遗余力地推广Java,宣称 Java applet 将会改变人们浏览网页的方式。然而市场并没有像 Sun 公司预期的那样好,这很大程度上是因为 Java applet 速度慢而且操作不便。网景公司的市场部门抓住了这个机遇,与 Sun 合作完成了 LiveScript 实现,并在网景的Navigator 2.0 发布前,将 LiveScript 更名为 JavaScript。网景公司为了取得 Sun 公司的支持,把 JavaScript 称为 Java applet 和 HTML 的补充工具,目的之一就是为了帮助开发者更好地操纵 Java applet。Netscape 决不会预料到当年那个市场策略带来的副作用有多大。多年来,到处都有人混淆 Java 和 JavaScript 这两个不相干的语言。两者除了名字相似和历史渊源之外,几乎没有任何关系。现在看来,从论坛到邮件列表,从网站到图书馆,能把 Java 和 JavaScript 区分开的倒是少数③。图1-3 是百度知道上的“Java 相关”分类。
① Brendan Eich 被人称为 JavaScript 之父,他完全没想到自己当年无心设计的一个语言会成为今天最流行的网络脚
本语言。
② applet 的意思是“小程序”,它是 Java 的一个客户端组件,需要在“容器”中运行,通常浏览器会充当这个容器。
③ Brendan Eich 为此抱憾不已,他后来在一个名为“ JavaScript at Ten Years”(JavaScript 这10年)的演讲稿中写道:
“ Don’t let marketing name your language.”(不要为了营销决定语言名称)。
8 第 1 章 Node.js 简介
1.5.3 微软的加入—— JScript
就在网景公司如日中天之时,微软的 Internet Explorer 3 随 Windows 95 OSR2 捆绑销售的策略堪称一颗重磅炸弹,轻松击败了强劲的对手——网景公司的Navigator。尽管这个做法致使微软后来声名狼藉(以及一系列的反垄断诉讼),但 Internet Explorer3 的成功却有目共睹,其成功不仅仅在于市场营销策略,也源于产品本身。Internet Explorer 3 是一个划时代产品,因为它也实现了类似于 JavaScript 的客户端语言—— JScript,除此之外还有微软的“老本行”VBScript。JScript 的诞生成为 JavaScript 发展的一个重要里程碑,标志了动态网页时代的全面到来。
1.5.4 标准化—— ECMAScript
最初 JavaScript 并没有一个标准,因此在不同浏览器间有各种各样的兼容性的问题。Internet Explorer 占领市场以后这个问题变得更加尖锐,因此 JavaScript 的标准化势在必行。在1996年,JavaScript 标准由诸多软件厂商共同提交给ECMA(欧洲计算机制造商协会)。ECMA 通过了标准 ECMA-262,也就是 ECMAScript。紧接着国际标准化组织也采纳了ECMAScript 标准(ISO-16262)。在接下来的几年里,浏览器开发者们就开始以 ECMAScript 作为规范来实现 JavaScript 解析引擎。
ECMAScript 诞生至今已经有了多个版本,最新的版本是在2009年12月发布的ECMAScript 5,而到2012年为止,业界普遍支持的仍是 ECMAScript 3,只有新版的 Chrome
和 Firefox 实现了 ECMAScript 5。ECMAScript 仅仅是一个标准,而不是一个语言的具体实现,而且这个标准不像 C++ 语言规范那样严格而详细。除了 JavaScript 之外,ActionScript①、QtScript②、WMLScript③也是 ECMAScript 的实现。
1.5.5 浏览器兼容性问题
尽管有 ECMAScript 作为 JavaScript 的语法和语言特性标准,但是关于 JavaScript 其他方面的规范还是不明确,同时不同浏览器又加入了各自特有的对象、函数。这也就是为什么这么多年来同样的 JavaScript 代码会在不同的浏览器中呈现出不同的效果,甚至在一个浏览器中可以执行,而在另一个浏览器中却不可以。要注意的是,浏览器的兼容性问题并不只是由 JavaScript 的兼容性造成的,而是 DOM、BOM、CSS 解析等不同的行为导致的。万维网联盟(World Wide Web Consortium,W3C)
针对这个问题提出了很多标准建议,目前已经几乎被所有厂商和社区接受,浏览器的兼容性问题迅速得到了改善。
1.5.6 引擎效率革命和 JavaScript 的未来
第一款 JavaScript 引擎是由 Brendan Eich 在网景的 Navigator 中开发的,它的名字叫做SpiderMonkey。SpiderMonkey 在这之后还用作 Mozilla Firefox 1.0~3.0版本的引擎,而从Firefox 3.5 开始换为 TraceMonkey,4.0版本以后又换为 JaegerMonkey。Google Chrome 的JavaScript 引擎是 V8,同时 V8 也是 Node.js 的引擎。微软从 Internet Explorer 9 开始使用其新的 JavaScript 引擎 Chakra。
过去,JavaScript 一直不被人重视,很大程度上是因为它效率不高——不仅速度慢,还占用大量内存。但如今JavaScript的效率却令人刮目相看。历史总是如此相似,正如没有
Shockley 发明晶体管就没有电子科技革命一样,如果没有2008年以来的 JavaScript 引擎革命,Node.js 也不会这么快诞生。
——————————
① ActionScript 最初是 Adobe 公司 Flash 的一部分,用于控制动画效果,现在已经被广泛应用在 Adobe 的各项产品中。
② QtScript 是 Qt 4.3.0 以后引入的专用脚本工具。
③ WMLScript 是 WAP 协议的一部分,用于扩展 WML(Wireless Markup Language)页面。
④ 除此以外还有 KJS(用于 Konqueror)、Nitro(用于 Safari)、Carakan (用于Opera)等 JavaScript 引擎。
10 第 1 章 Node.js 简介
2008年 Mozilla Firefox 的一次改动,使 Firefox 3.0的 JavaScript 性能大幅提升,从而引发了 JavaScript 引擎之间的效率竞赛。紧接着 WebKit①开发团队宣告了 Safari 4 新的 JavaScript 引擎 SquirrelFish(后来改名 Nitro)可以大幅度提升脚本执行速度。Google Chrome 刚刚诞生就因它的 JavaScript 性能而备受称赞,但随着 WebKit 的 Squirrelfish Extreme 和 Mozilla 的TraceMonkey 技术的出现,Chrome 的 JavaScript 引擎速度被超越了,于是 Chrome 2 发布时使用了更快速的 V8 引擎。V8 一出场就以其一骑绝尘般的速度打败了所有对手,一度成为JavaScript 引擎的速度之王。于是其他浏览器的开发者开始奋力追赶,与以往不同的是,Internet Explorer 也加入了这次竞赛,并取得了不俗的成绩。
时至今日,各个 JavaScript 引擎的效率已经不相上下,通过不同引擎根据不同测试基准测得的结果各有千秋。更有趣的是,JavaScript 的效率在不知不觉中已经超越了其他所有传统的脚本语言,并带动了解释器的革新运动。JavaScript 已经成为了当今速度最快的脚本语言之一,昔日“丑小鸭”终于成了惊艳绝俗的“白天鹅”。尽管如此,我们不能否认 JavaScript 还有很多不完美之处,譬如一些违反直觉的特性,这几乎成了 JavaScript 遭受批评和攻击的焦点。如今 JavaScript 还在继续发展,ECMAScript 6
也正在起草中,更有像 CoffeeScript 这样专门为了弥补 JavaScript 语言特性的不足而诞生的语言。Google 也专门针对客户端 JavaScript 不完美的地方推出了 Dart 语言。随着大规模的应用推广,我们有理由相信 JavaScript 会变得越来越好。
1.6 CommonJS
1.6.1 服务端 JavaScript 的重生
Node.js 并不是第一个尝试使 JavaScript 运行在浏览器之外的项目。追根溯源,在
JavaScript 诞生之初,网景公司就实现了服务端的 JavaScript,但由于需要支付一大笔授权费
用才能使用,服务端 JavaScript 在当年并没有像客户端 JavaScript 一样流行开来。真正使大
多数人见识到 JavaScript 在服务器开发威力的,是微软的 ASP。
2000年左右,也就是 ASP 蒸蒸日上的年代,很多开发者开始学习 JScript。然而 JScript 在
当时并不是很受欢迎,一方面是早期的 JScript 和 JavaScript 兼容较差,另一方面微软大力推
广的是 VBScript,而不是 JScript。随着后来 LAMP 的兴起,以及Web 2.0 时代的到来,Ajax
等一系列概念的提出,JavaScript 成了前端开发的代名词,同时服务端 JavaScript 也逐渐被人
遗忘。
——————————
① WebKit 是苹果公司在设计 Safari 时开发的浏览器引擎,起源于 KHTML 和 KJS 项目的分支。WebKit 包含了一个
网页引擎 WebCore 和一个脚本引擎 JavaScriptCore,但由于 JavaScript 引擎越来越独立,WebKit 逐渐成为了
WebCore 的代名词。
1.6 CommonJS 11
直至几年前,JavaScript 的种种优势才被重新提起,JavaScript 又具备了在服务端流行的
条件,Node.js 应运而生。与此同时,RingoJS 也基于 Rhino 实现了类似的服务端 JavaScript 平
台,还有像 CouchDB、MongoDB 等新型非关系型数据库也开始用 JavaScript 和 JSON 作为
其数据操纵语言,基于 JavaScript 的服务端实现开始遍地开花。
1.6.2 CommonJS 规范与实现
正如当年为了统一 JavaScript 语言标准,人们制定了 ECMAScript 规范一样,如今为了
统一 JavaScript 在浏览器之外的实现,CommonJS 诞生了。CommonJS 试图定义一套普通应
用程序使用的API,从而填补 JavaScript 标准库过于简单的不足。CommonJS 的终极目标是
制定一个像 C++ 标准库一样的规范,使得基于 CommonJS API 的应用程序可以在不同的环
境下运行,就像用 C++ 编写的应用程序可以使用不同的编译器和运行时函数库一样。为了
保持中立,CommonJS 不参与标准库实现,其实现交给像 Node.js 之类的项目来完成。图1-5
是 CommonJS 的各种实现。
CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、
控制台(console)、编码(encodings)、文件系统(filesystems)、套接字(sockets)、单元测
试(unit testing)等部分。目前大部分标准都在拟定和讨论之中,已经发布的标准有
Modules/1.0、Modules/1.1、Modules/1.1.1、Packages/1.0、System/1.0。
12 第 1 章 Node.js 简介
Node.js 是目前 CommonJS 规范最热门的一个实现,它基于 CommonJS 的 Modules/1.0 规
范实现了 Node.js 的模块,同时随着 CommonJS 规范的更新,Node.js 也在不断跟进。由于目
前 CommonJS 大部分规范还在起草阶段,Node.js 已经率先实现了一些功能,并将其反馈给
CommonJS 规范制定组织,但 Node.js 并不完全遵循 CommonJS 规范。这是所有规范制定者
都会遇到的尴尬局面,因为规范的制定总是滞后于技术的发展。
1.7 参考资料
Node.js: http://nodejs.org/。
“再谈select、iocp、epoll、kqueue及各种I/O复用机制”: http://blog.csdn.net/shallwake/
article/details/5265287。
“巅峰对决:node.js和php性能测试”: http://snoopyxdy.blog.163.com/blog/static/6011744
0201183101319257/。
“RingoJS vs. Node.js: Runtime Values”: http://hns.github.com/2010/09/21/benchmark. html。
“Update on my Node.js Memory and GC Benchmark”: http://hns.github.com/2010/ 09/29/
benchmark2.html。
“JavaScript at Ten Years”: http://dl.acm.org/citation.cfm?id=1086382。
QtScript : http://qt-project.org/doc/qt-4.8/qtscript.html。
WebKit Open Source Project : http://www.webkit.org/。
CommonJS API Specifications : http://www.commonjs.org/specs/。
RingoJS : http://ringojs.org/。
MongoDB : http://www.mongodb.org/。
CouchDB : http://couchdb.apache.org/。
Persevere : http://www.persvr.org/。
《JavaScript 语言精髓与编程实践》 周爱民著,电子工业出版社出版。
《JavaScript 高级程序设计(第3版)》 Nicholas C. Zakas 著,人民邮电出版社出版。
《JavaScript 权威指南(第5版)》 Flanagan David 著,机械工业出版社出版。
2.1 安装前的准备 13
安装和配置Node.js
第 2 章
14 第 2 章 安装和配置 Node.js
在使用 Node.js 开发之前,我们首先要配置好开发环境。本章的主要内容有:
如何在 Linux、Windows、Mac OS X 上通过包或包管理器安装 Node.js ;
如何在 POSIX 和 Windows 下通过编译源代码安装 Node.js ;
安装 npm(Node.js 包管理器);
使用多版本管理器让多个 Node.js 的实例共存。
2.1 安装前的准备
Node.js 的生态系统建立在遵循 POSIX 标准的操作系统上,如 GNU/Linux、Mac OS X、
Solaris 等。Node.js 起初不支持 Windows,只能运行在 cygwin 上,而0.6版本以后就支持
Windows 了,本节后面会详述。
从2009年诞生至今,Node.js 一直处在快速发展的时期,因此很多方法、技巧都会迅速
被新的技术取代,本书内容也不例外。就在不久前,大家还都推荐通过编译源代码安装
Node.js,而现在已经有了成熟的安装包发行系统。我们推荐你尽量通过 Node.js 官方或操作
系统发行版提供的途径进行安装,除非你想获得最新的版本,否则就不要费力编译了。
Windows 上的 Node.js
Node.js 从0.6版本开始可以运行在原生的 Windows 上了(不是 cygwin 或者其他虚拟环
境)。这很大程度上应该归功于微软的合作,因为微软的云计算平台 Windows Azure 宣布了
对 Node.js 完全支持。这对微软来说简直是破天荒的举动,因为一贯具有“开源死敌”之称
的微软,竟然支持具有深厚开源血统的 Node.js,不得不令人瞠目结舌。
尽管如此,Node.js 与 Windows 的兼容性仍然不如 POSIX 操作系统,这一点在 npm 提
供的第三方模块中体现得尤为突出。这主要是因为许多第三方的模块需要编译原生的 C/C++
代码,其中编译框架和系统调用很多都是以 Linux 为范本的,与 Windows 不兼容。笔者不建
议在 Windows 上进行 Node.js 开发或部署,当然出于学习目的,这些影响也是无关紧要的。
相信随着 Node.js 的发展(以及微软与开源社区关系的进一步改善),Node.js 与 Windows 的
兼容性会越来越好。
接下来的小节我们将详细介绍 Node.js 的安装方法。
2.2 快速安装
2.2.1 Microsoft Windows系统上安装Node.js
在 Windows 上安装 Node.js 十分方便,你只需要访问http://nodejs.org,点击Download链
接,然后选择Windows Installer,下载安装包。下载完成后打开安装包(如图2-1所示),点击
Next即可自动完成安装。
图2-1 在 Windows 上安装 Node.js
安装程序不会询问你安装路径,Node.js 会被自动安装到 C:\Program Files\nodejs 或
C:\Program Files (x86)\nodejs(64位系统)目录下,并且会在系统的 PATH 环境变量中增加该
目录,因此我们可以在 Windows 的命令提示符中直接运行 node。
为了测试是否已经安装成功,我们在运行中输入 cmd,打开命令提示符,然后输入 node,
将会进入 Node.js 的交互模式,如图2-2所示。
图2-2 Windows 命令提示符下的 Node.js
通过这种方式安装的 Node.js 还自动附带了 npm 图2-2,我们可以在命令提示符中直接
输入 npm 来使用它。
16 第 2 章 安装和配置 Node.js
2.2.2 Linux 发行版上安装Node.js
Node.js 目前还处在快速变化的时期,它的发行速度要远远大于 Linux 发行版维护的周
期,因此各个 Linux 发行版官方的软件包管理器中提供的 Node.js 往往都比较过时。尽管如
此,我们还是可以通过发行版的包管理器获得一个较为稳定的版本,根据不同的发行版,通
过以下命令来获取Node.js,参见表2-1。
表2-1 在 Linux 发行版中获取 Node.js
Linux 发行版 命 令
Debian/Ubuntu apt-get install nodejs
Fedora/RHEL/CentOS/Scientific Linux yum install nodejs
openSUSE zypper install nodejs
Arch Linux pacman -S nodejs
如果你需要用软件包管理器来获得较新版本的 Node.js,就要根据不同的发行版选择第
三方的软件源,具体请参阅:https://github.com/joyent/node/wiki/Installing-Node.js-via-packagemanager。
2.2.3 Mac OS X上安装Node.js
Node.js 官方专门提供了 Mac OS X 的安装包,你可以在 http://nodejs.org 找到Download
链接,然后选择Macintosh Installer,下载安装包。下载完成后运行安装包(如图2-3 所示),
根据提示完成安装。
图2-3 在 Mac OS X 上安装 Node.js
Node.js 和 npm 会被安装到 /usr/local/bin 目录下,安装过程中需要系统管理员权限。安
装成功后你可以在终端机中运行 node 命令进入了 Node.js 的交互模式。如果出现 -bash:
node: command not found,说明没有正确安装,需要重新运行安装包或者采取其他形式
安装 Node.js。
2.3 编译源代码
Node.js 从 0.6 版本开始已经实现了源代码级别的跨平台,因此我们可以使用不同的编译
命令将同一份源代码的基础上编译为不同平台下的原生可执行代码。
在编译之前,要先获取源码包。我们建议访问http://nodejs.org,点击Download链接,然
后选择Source Code,下载正式发布的源码包。如果你需要开发中的版本,可以通过
https://github.com/joyent/node/zipball/master 获得,或者在命令行下输入git clone git:
//github.com/joyent/node.git 从git获得最新的分支。
2.3.1 在 POSIX 系统中编译
在 POSIX 系统中编译 Node.js 需要三个工具:
C++编译器 gcc 或 clang/LLVM;
Python 版本 2.5 以上,不支持 Python 3;
libssl-dev 提供 SSL/TLS 加密支持。
如果你使用 Linux,那么你需要使用 g++ 来编译 Node.js。在 Debian/Ubuntu 中,你可以
通过 apt-get install g++ 命令安装g++。在 Fedora/Redhat/CentOS 中,你可以使用 yum
install gcc-c++ 安装。
如果使用的是 Mac OS X,那么需要安装 Xcode。默认情况下,系统安装盘中会有 Xcode,
可以从光盘中安装,或者访问 https://developer.apple.com/xcode/ 下载最新的版本。
Mac OS X 和几乎所有的 Linux 发行版都内置了 Python,你可以在终端机输入命令
python --version 检查 Python 的版本,可能会显示 Python 2.7.2 或其他版本。如果你
发现版本号小于2.5或者直接出现了 command not found,那么你需要通过软件包管理器
获得一个新版本的 Python,或者到 http://python.org/ 下载一个。
libssl-dev 是调用 OpenSSL 编译所需的头文件,用于提供 SSL/TLS 加密支持。Mac OS
X 的 Xcode 内置了 libssl-dev。在 Debian/Ubuntu 中,你可以通过 apt-get install
libssl-dev 命令安装。在 Fedora/Redhat/CentOS 中,你可以通过 yum install
openssl-devel 命令安装。同样,你也可以访问 http://openssl.org/ 下载一个。
接下来,进入 Node.js 源代码所在目录,运行:
./configure
make
sudo make install
18 第 2 章 安装和配置 Node.js
之后大约等待20分钟,Node.js 就安装完成了,而且附带安装了 npm。
如果你使用 Mac OS X,还可以尝试使用 homebrew 编译安装 Node.js。首先在 http://mxcl.
github.com/homebrew/获取 homebrew,然后通过以下命令即可自动解析编译依赖并安装Node.js:
brew install node
2.3.2 在 Windows系统中编译
Node.js 在 Windows 下只能通过 Microsoft Visual Studio 编译,因此你需要首先安装 Visual
Studio 或者免费的 Visual Studio Express。你还需要安装 Python 2(2.5以上的版本,但要小于
3.0),可以在http://python.org/取得。安装完 Python 以后请确保在PATH环境变量中添加
python.exe 所在的目录,如果没有则需要手动在“系统属性”中添加。
一切准备好以后,打开命令提示符,进入 Node.js 源代码所在的目录进行编译:
C:\Users\byvoid\node-v0.6.12>vcbuild.bat release
['-f', 'msvs', '-G', 'msvs_version=2010', '.\\node.gyp', '-I', '.\\common.gypi', '--depth=.',
'-Dtarget_Project files generated.
C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\Microsoft.CppBuild.targets(1151,5):
warning MSB8012: http_parser.vcxproj -> C:\Users\byvoid\node-v0.6.12\
Release\http_parser.lib
js2c, and also js2c_experimental
node_js2c
...
大约等待20分钟,编译完成。在 Release 子目录下面会有一个 node.exe 文件,这就是我
们编译的唯一目标。也许有些令人惊讶,Node.js 编译后只有一个 node.exe文件,这说明 Node.js
的核心非常小巧精悍。直接运行 node.exe 即可进入 Node.js 的交互模式,在系统 PATH 环境
变量中添加node.exe文件所在的目录,这样就可以在命令行中运行 node 命令了,剩下的工
作就是手动安装 npm 了。
2.4 安装 Node 包管理器
Node 包管理器(npm)是一个由 Node.js 官方提供的第三方包管理工具,就像 PHP 的
Pear、Python 的 PyPI 一样。npm 是一个完全由 JavaScript 实现的命令行工具,通过 Node.js 执
行,因此严格来讲它不属于 Node.js 的一部分。在最初的版本中,我们需要在安装完 Node.js
以后手动安装npm。但从 Node.js 0.6 开始,npm 已包含在发行包中了,我们在 Windows、
Mac 上安装包和源代码包时会自动同时安装 npm。
如果你是在 Windows 下手动编译的,或是在 POSIX 系统中编译时指定了 --without-npm
参数,那就需要手动安装 npm 了。http://npmjs.org/提供了 npm 几种不同的安装方法,通常
你只需要执行以下命令:
curl http://npmjs.org/install.sh | sh
如果安装过程中出现了权限问题,那么需要在 root 权限下执行上面的语句,或者使用sudo。
curl http://npmjs.org/install.sh | sudo sh
其他安装方法,譬如从 git 中获取 npm 的最新分支,可以参考 http://npmjs.org/doc/
README.html上的说明。
2.5 安装多版本管理器
迄今为止Node.js 更新速度还很快,有时候新版本还会将旧版本的一些 API 废除,以至
于写好的代码不能向下兼容。有时候你可能想要尝试一下新版本有趣的特性,但又想要保持
一个相对稳定的环境。基于这种需求,Node.js 的社区开发了多版本管理器,用于在一台机
器上维护多个版本的 Node.js 实例,方便按需切换。Node 多版本管理器(Node Version
Manager,nvm)是一个通用的叫法,它目前有许多不同的实现。通常我们说的 nvm 是指
https://github.com/creationix/nvm 或者 https://github.com/visionmedia/n。笔者根据个人偏好推
荐使用 visionmedia/n,此小节就以它为例子介绍 Node 多版本管理器的用法。
n 是一个十分简洁的 Node 多版本管理器,就连它的名字也不例外。它的名字就是 n,
没错,就一个字母。①
如果你已经安装好了 Node.js 和 npm 环境,就可以直接使用 npm install -g n 命令
来安装 n。当然你可能会问:如果我想完全通过 n 来管理 Node.js,那么没安装之前哪来的 npm
呢?事实上,n 并不需要 Node.js 驱动,它只是 bash 脚本,使用 npm 安装只是采取一种简便
的方式而已。我们可以在 https://github.com/visionmedia/n 下载它的代码,然后使用 make
install 命令安装。
n 不支持 Windows。
安装完 n 以后,在终端中运行 n --help 即可看到它的使用说明:
$ n --help
Usage: n [options] [COMMAND] [config]
——————————
① 事实上,n 它曾经叫做 nvm,后来改名为 n。
20 第 2 章 安装和配置 Node.js
Commands:
n Output versions installed
n latest [config ...] Install or activate the latest node release
n <version> [config ...] Install and/or use node <version>
n use <version> [args ...] Execute node <version> with [args ...]
n bin <version> Output bin path for <version>
n rm <version ...> Remove the given version(s)
n --latest Output the latest node version available
n ls Output the versions of node available
Options:
-V, --version Output current version of n
-h, --help Display help information
Aliases:
- rm
which bin
use as
list ls
运行 n 版本号 可以安装任意已发布版本的 Node.js,n 会从http://nodejs.org下载源代码包,
然后自动编译安装,例如:
$ n 0.7.5
######################################################################## 100.0%
{ 'target_defaults': { 'cflags': [],
'defines': [],
'include_dirs': [],
'libraries': ['-lz']},
'variables': { 'host_arch': 'x64',
'node_install_npm': 'true',
'node_install_waf': 'true',
'node_prefix': '/usr/local/n/versions/0.7.5',
'node_shared_cares': 'false',
'node_shared_v8': 'false',
'node_use_dtrace': 'false',
'node_use_openssl': 'true',
'node_use_system_openssl': 'false',
'target_arch': 'x64',
'v8_use_snapshot': 'true'}}
creating ./config.gypi
creating ./config.mk
make -C out BUILDTYPE=Release
CC(target) /usr/local/n/node-v0.7.5/out/Release/obj.target/http_parser/deps/
http_parser/http_parser.o
LIBTOOL-STATIC /usr/local/n/node-v0.7.5/out/Release/libhttp_parser.a
...
通过 n 获取的 Node.js 实例都会安装在 /usr/local/n/versions/ 目录中。
之后再运行 n 即可列出已经安装的所有版本的 Node.js,其中“*”后的版本号为默认的
Node.js 版本,即可以直接使用 node 命令行调用的版本:
$ n
0.6.11
* 0.7.5
和安装新版本一样,运行 n 版本号 也可以在已安装的 Node.js 实例中切换环境,再运行
node 即为 n 指定的当前版本,例如:
$ n 0.6.11
* 0.6.11
0.7.5
$ node -v
v0.6.11
如果你不想切换默认环境,可以使用 n use 版本号 script.js 直接指定Node.js的运
行实例,例如:
$ n use 0.6.11 script.js
n 无法管理通过其他方式安装的 Node.js 版本实例(如官方提供的安装
包、发行版软件源、手动编译),你必须通过 n 安装 Node.js 才能管理多版
本的 Node.js。
关于 n 的更多细节,请访问它的项目主页 https://github.com/visionmedia/n获取信息。
2.6 参考资料
“Building and Installing Node.js”: https://github.com/joyent/node/wiki/Installation。
“Node package manager”: http://npmjs.org/doc/README.html。
“Node version management”: https://github.com/visionmedia/n。
22 第 2 章 安装和配置 Node.js
“深入浅出Node.js(二): Node.js & NPM的安装与配置”: http://www.infoq.com/cn/
articles/nodejs-npm-install-config。
“Node.js Now Runs Natively on Windows”: http://www.infoq.com/news/2011/11/NodejsWindows。
《Node Web开发》,David Herron著,人民邮电出版社出版。
“如何在 Mac OS X Lion 上设定 node.js 的开发环境”: http://dreamerslab.com/blog/tw/
how-to-setup-a-node- js-development-environment-on-mac-osx-lion/。
Node.js快速入门
第 3 章
24 第 3 章 Node.js 快速入门
Node.js 是一个方兴未艾的技术。一直以来,关于 Node.js 的宣传往往针对它“与众不同”
的特性,这使得它显得格外扑朔迷离。事实上,Node.js的绝大部分特性跟大多数语言一样都
是旧瓶装新酒,只是一些激进的特性使它显得很神秘。在这一章中,我们将会讲述Node.js的
种种特性,让你对 Node.js 本身以及如何使用 Node.js 编程有一个全局性的了解,主要内容有:
编写第一个Node.js程序;
异步式I/O和事件循环;
模块和包;
调试。
让我们开始这个激动人心的旅程吧。
3.1 开始用 Node.js 编程
Node.js 具有深厚的开源血统,它诞生于托管了许多优秀开源项目的网站—— github。和
大多数开源软件一样,它由一个黑客发起,然后吸引了一小拨爱好者参与贡献代码。一开始
它默默无闻,靠口口相传扩散,直到某一天被一个黑客媒体曝光,进入业界视野,随后便有
一些有远见的公司提供商业支持,使其逐步发展壮大。
用 Node.js 编程是一件令人愉快的事情,因为你将开始用黑客的思维和风格编写代码。
你会发现像这样的语言是很容易入门的,可以快速了解到它的细节,然后掌握它。
3.1.1 Hello World
好了,让我们开始实现第一个 Node.js 程序吧。打开你常用的文本编辑器,在其中输入:
console.log('Hello World');
将文件保存为 helloworld.js,打开终端,进入 helloworld.js 所在的目录,执行以下命令:
node helloworld.js
如果一切正常,你将会在终端中看到输出 Hello World。很简单吧?下面让我们来解
释一下这个程序的细节。console 是 Node.js 提供的控制台对象,其中包含了向标准输出写
入的操作,如 console.log、console.error 等。console.log 是我们最常用的输出
指令,它和 C 语言中的 printf 的功能类似,也可以接受任意多个参数,支持 %d、%s 变
量引用,例如:
//consolelog.js
console.log('%s: %d', 'Hello', 25);
输出的是 Hello: 25。这只是一个简单的例子,如果你想了解 console 对象的详细功能,
请参见 4.1.3节。
3.1.2 Node.js 命令行工具
在前面的 Hello World 示例中,我们用到了命令行中的 node 命令,输入 node --help
可以看到详细的帮助信息:
Usage: node [options] [ -e script | script.js ] [arguments]
node debug script.js [arguments]
Options:
-v, --version print node's version
-e, --eval script evaluate script
-p, --print print result of --eval
--v8-options print v8 command line options
--vars print various compiled-in variables
--max-stack-size=val set max v8 stack size (bytes)
Environment variables:
NODE_PATH ';'-separated list of directories
prefixed to the module search path.
NODE_MODULE_CONTEXTS Set to 1 to load modules in their own
global contexts.
NODE_DISABLE_COLORS Set to 1 to disable colors in the REPL
Documentation can be found at http://nodejs.org/
其中显示了 node 的用法,运行 Node.js 程序的基本方法就是执行 node script.js,
其中 script.js①是脚本的文件名。
除了直接运行脚本文件外,node --help 显示的使用方法中说明了另一种输出 Hello
World 的方式:
$ node -e "console.log('Hello World');"
Hello World
我们可以把要执行的语句作为 node -e 的参数直接执行。
使用 node 的 REPL 模式
REPL (Read-eval-print loop),即输入—求值—输出循环。如果你用过 Python,就会知
道在终端下运行无参数的 python 命令或者使用 Python IDLE 打开的 shell,可以进入一个即
时求值的运行环境。Node.js 也有这样的功能,运行无参数的 node 将会启动一个 JavaScript
的交互式 shell:
——————————
① 事实上脚本文件的扩展名不一定是 .js,例如我们将脚本保存为 script.txt,使用 node script.txt 命令同样可
以运行。扩展名使用 .js 只是一个约定而已,遵循了 JavaScript 脚本一贯的命名习惯。
26 第 3 章 Node.js 快速入门
$ node
> console.log('Hello World');
Hello World
undefined
> consol.log('Hello World');
ReferenceError: consol is not defined
at repl:1:1
at REPLServer.eval (repl.js:80:21)
at repl.js:190:20
at REPLServer.eval (repl.js:87:5)
at Interface.<anonymous> (repl.js:182:12)
at Interface.emit (events.js:67:17)
at Interface._onLine (readline.js:162:10)
at Interface._line (readline.js:426:8)
at Interface._ttyWrite (readline.js:603:14)
at ReadStream.<anonymous> (readline.js:82:12)
进入 REPL 模式以后,会出现一个“>”提示符提示你输入命令,输入后按回车,Node.js
将会解析并执行命令。如果你执行了一个函数,那么 REPL 还会在下面显示这个函数的返回
值,上面例子中的 undefined 就是 console.log 的返回值。如果你输入了一个错误的
指令,REPL 则会立即显示错误并输出调用栈。在任何时候,连续按两次 Ctrl + C 即可推出
Node.js 的 REPL 模式。
node 提出的 REPL 在应用开发时会给人带来很大的便利,例如我们可以测试一个包能
否正常使用,单独调用应用的某一个模块,执行简单的计算等。
3.1.3 建立 HTTP 服务器
前面的 Hello World 程序对于你来说可能太简单了,因为这个例子几乎可以在任何语言
的教科书上找到对应的内容,既无聊又乏味,让我们来点儿不一样的东西,真正感受一下
Node.js 的魅力所在吧。
Node.js 是为网络而诞生的平台,但又与 ASP、PHP 有很大的不同,究竟不同在哪里呢?
如果你有 PHP 开发经验,会知道在成功运行 PHP 之前先要配置一个功能强大而复杂的 HTTP
服务器,譬如 Apache、IIS 或 Nginx,还需要将 PHP 配置为 HTTP 服务器的模块,或者使用
FastCGI 协议调用 PHP 解释器。这种架构是“浏览器 HTTP 服务器 PHP 解释器”的组织
方式,而Node.js采用了一种不同的组织方式,如图3-1 所示。
我们看到,Node.js 将“HTTP服务器”这一层抽离,直接面向浏览器用户。这种架构
从某种意义上来说是颠覆性的,因而会让人心存疑虑:Node.js作为HTTP服务器的效率
足够吗?会不会提高耦合程度?我们不打算在这里讨论这种架构的利弊,后面章节会继续
说明。
图3-1 Node.js 与 PHP 的架构
好了,回归正题,让我们创建一个 HTTP 服务器吧。建立一个名为 app.js 的文件,内容
为:
//app.js
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>Hello World</p>');
}).listen(3000);
console.log("HTTP server is listening at port 3000.");
接下来运行 node app.js命令,打开浏览器访问 http://127.0.0.1:3000,即可看到图3-2
所示的内容。
图3-2 用 Node.js 实现的 HTTP 服务器
28 第 3 章 Node.js 快速入门
用 Node.js 实现的最简单的 HTTP 服务器就这样诞生了。这个程序调用了 Node.js 提供的
http 模块,对所有 HTTP 请求答复同样的内容并监听 3000 端口。在终端中运行这个脚本
时,我们会发现它并不像 Hello World 一样结束后立即退出,而是一直等待,直到按下 Ctrl +
C 才会结束。这是因为 listen 函数中创建了事件监听器,使得 Node.js 进程不会退出事件
循环。我们会在后面的章节中详细介绍这其中的奥秘。
小技巧——使用 supervisor
如果你有 PHP 开发经验,会习惯在修改 PHP 脚本后直接刷新浏览器以观察结果,而你
在开发 Node.js 实现的 HTTP 应用时会发现,无论你修改了代码的哪一部份,都必须终止
Node.js 再重新运行才会奏效。这是因为 Node.js 只有在第一次引用到某部份时才会去解析脚
本文件,以后都会直接访问内存,避免重复载入,而 PHP 则总是重新读取并解析脚本(如
果没有专门的优化配置)。Node.js的这种设计虽然有利于提高性能,却不利于开发调试,因
为我们在开发过程中总是希望修改后立即看到效果,而不是每次都要终止进程并重启。
supervisor 可以帮助你实现这个功能,它会监视你对代码的改动,并自动重启 Node.js。
使用方法很简单,首先使用 npm 安装 supervisor:
$ npm install -g supervisor
如果你使用的是 Linux 或 Mac,直接键入上面的命令很可能会有权限错误。原因是 npm
需要把 supervisor 安装到系统目录,需要管理员授权,可以使用 sudo npm install -g
supervisor 命令来安装。
接下来,使用 supervisor 命令启动 app.js:
$ supervisor app.js
DEBUG: Running node-supervisor with
DEBUG: program 'app.js'
DEBUG: --watch '.'
DEBUG: --extensions 'node|js'
DEBUG: --exec 'node'
DEBUG: Starting child process with 'node app.js'
DEBUG: Watching directory '/home/byvoid/.' for changes.
HTTP server is listening at port 3000.
当代码被改动时,运行的脚本会被终止,然后重新启动。在终端中显示的结果如下:
DEBUG: crashing child
DEBUG: Starting child process with 'node app.js'
HTTP server is listening at port 3000.
supervisor 这个小工具可以解决开发中的调试问题。
3.2 异步式 I/O 与事件式编程
Node.js 最大的特点就是异步式 I/O(或者非阻塞 I/O)与事件紧密结合的编程模式。这
种模式与传统的同步式 I/O 线性的编程思路有很大的不同,因为控制流很大程度上要靠事件
和回调函数来组织,一个逻辑要拆分为若干个单元。
3.2.1 阻塞与线程
什么是阻塞(block)呢?线程在执行中如果遇到磁盘读写或网络通信(统称为 I/O 操作),
通常要耗费较长的时间,这时操作系统会剥夺这个线程的 CPU 控制权,使其暂停执行,同
时将资源让给其他的工作线程,这种线程调度方式称为 阻塞。当 I/O 操作完毕时,操作系统
将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种 I/O 模式就是通
常的同步式 I/O(Synchronous I/O)或阻塞式 I/O (Blocking I/O)。
相应地,异步式 I/O (Asynchronous I/O)或非阻塞式 I/O (Non-blocking I/O)则针对
所有 I/O 操作不采用阻塞的策略。当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作
的完成或数据的返回,而只是将 I/O 请求发送给操作系统,继续执行下一条语句。当操作
系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个
事件。为了处理异步 I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予
以处理。
阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞
模式下,一个线程永远在执行计算操作,这个线程所使用的 CPU 核心利用率永远是 100%,
I/O 以事件的方式通知。在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞
时还有其他线程在工作,多线程可以让 CPU 资源不被阻塞中的线程浪费。而在非阻塞模式
下,线程不会被 I/O 阻塞,永远在利用 CPU。多线程带来的好处仅仅是在多核 CPU 的情况
下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么 Node.js 使用了单
线程、非阻塞的事件编程模式。
图3-3 和图3-4 分别是多线程同步式 I/O 与单线程异步式 I/O 的示例。假设我们有一项工
作,可以分为两个计算部分和一个 I/O 部分,I/O 部分占的时间比计算多得多(通常都是这
样)。如果我们使用阻塞 I/O,那么要想获得高并发就必须开启多个线程。而使用异步式 I/O
时,单线程即可胜任。
30 第 3 章 Node.js 快速入门
图3-3 多线程同步式 I/O
图3-4 单线程异步式 I/O
单线程事件驱动的异步式 I/O 比传统的多线程阻塞式 I/O 究竟好在哪里呢?简而言之,
异步式 I/O 就是少了多线程的开销。对操作系统来说,创建一个线程的代价是十分昂贵的,
需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被
清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。①
当然,异步式编程的缺点在于不符合人们一般的程序设计思维,容易让控制流变得晦涩
难懂,给编码和调试都带来不小的困难。习惯传统编程模式的开发者在刚刚接触到大规模的异
步式应用时往往会无所适从,但慢慢习惯以后会好很多。尽管如此,异步式编程还是较为困难,
不过可喜的是现在已经有了不少专门解决异步式编程问题的库(如async),参见6.2.2节。
表3-1比较了同步式 I/O 和异步式 I/O 的特点。
表3-1 同步式 I/O 和异步式 I/O 的特点
同步式 I/O(阻塞式) 异步式 I/O(非阻塞式)
利用多线程提供吞吐量 单线程即可实现高吞吐量
通过事件片分割和线程调度利用多核CPU 通过功能划分利用多核CPU
需要由操作系统调度多线程使用多核 CPU 可以将单进程绑定到单核 CPU
难以充分利用 CPU 资源 可以充分利用 CPU 资源
内存轨迹大,数据局部性弱 内存轨迹小,数据局部性强
符合线性的编程思维 不符合传统编程思维
3.2.2 回调函数
让我们看看在 Node.js 中如何用异步的方式读取一个文件,下面是一个例子:
//readfile.js
var fs = require('fs');
fs.readFile('file.txt', 'utf-8', function(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
console.log('end.');
运行的结果如下:
end.
Contents of the file.
——————————
① 基于多线程的模型也有相应的解决方案,如轻量级线程(lightweight thread)等。事件驱动的单线程异步模型与多线
程同步模型到底谁更好是一件非常有争议的事情,因为尽管消耗资源,后者的吞吐率并不比前者低。
32 第 3 章 Node.js 快速入门
Node.js 也提供了同步读取文件的 API:
//readfilesync.js
var fs = require('fs');
var data = fs.readFileSync('file.txt', 'utf-8');
console.log(data);
console.log('end.');
运行的结果与前面不同,如下所示:
$ node readfilesync.js
Contents of the file.
end.
同步式读取文件的方式比较容易理解,将文件名作为参数传入 fs.readFileSync 函
数,阻塞等待读取完成后,将文件的内容作为函数的返回值赋给 data 变量,接下来控制台
输出 data 的值,最后输出 end.。
异步式读取文件就稍微有些违反直觉了,end.先被输出。要想理解结果,我们必须先
知道在 Node.js 中,异步式 I/O 是通过回调函数来实现的。fs.readFile 接收了三个参数,
第一个是文件名,第二个是编码方式,第三个是一个函数,我们称这个函数为回调函数。
JavaScript 支持匿名的函数定义方式,譬如我们例子中回调函数的定义就是嵌套在
fs.readFile 的参数表中的。这种定义方式在 JavaScript 程序中极为普遍,与下面这种定义
方式实现的功能是一致的:
//readfilecallback.js
function readFileCallBack(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
}
var fs = require('fs');
fs.readFile('file.txt', 'utf-8', readFileCallBack);
console.log('end.');
fs.readFile 调用时所做的工作只是将异步式 I/O 请求发送给了操作系统,然后立即
返回并执行后面的语句,执行完以后进入事件循环监听事件。当 fs 接收到 I/O 请求完成的
事件时,事件循环会主动调用回调函数以完成后续工作。因此我们会先看到 end.,再看到
file.txt 文件的内容。
Node.js 中,并不是所有的 API 都提供了同步和异步版本。Node.js 不
鼓励使用同步 I/O。
3.2.3 事件
Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。在开发者看来,事
件由 EventEmitter 对象提供。前面提到的 fs.readFile 和 http.createServer 的回
调函数都是通过 EventEmitter 来实现的。下面我们用一个简单的例子说明 EventEmitter
的用法:
//event.js
var EventEmitter = require('events').EventEmitter;
var event = new EventEmitter();
event.on('some_event', function() {
console.log('some_event occured.');
});
setTimeout(function() {
event.emit('some_event');
}, 1000);
运行这段代码,1秒后控制台输出了 some_event occured.。其原理是 event 对象
注册了事件 some_event 的一个监听器,然后我们通过 setTimeout 在1000毫秒以后向
event 对象发送事件 some_event,此时会调用 some_event 的监听器。
我们将在 4.3.1节中详细讨论 EventEmitter 对象的用法。
Node.js 的事件循环机制
Node.js 在什么时候会进入事件循环呢?答案是 Node.js 程序由事件循环开始,到事件循
环结束,所有的逻辑都是事件的回调函数,所以 Node.js 始终在事件循环中,程序入口就是
事件循环第一个事件的回调函数。事件的回调函数在执行的过程中,可能会发出 I/O 请求或
直接发射(emit)事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未
处理的事件,直到程序结束。图3-5说明了事件循环的原理。
与其他语言不同的是,Node.js 没有显式的事件循环,类似 Ruby 的 EventMachine::run()
的函数在 Node.js 中是不存在的。Node.js 的事件循环对开发者不可见,由 libev 库实现。libev
支持多种类型的事件,如 ev_io、ev_timer、ev_signal、ev_idle 等,在 Node.js 中均被
EventEmitter 封装。libev 事件循环的每一次迭代,在 Node.js 中就是一次 Tick,libev 不
断检查是否有活动的、可供检测的事件监听器,直到检测不到时才退出事件循环,进程结束。
34 第 3 章 Node.js 快速入门
图3-5 事件循环
3.3 模块和包
模块(Module)和包(Package)是 Node.js 最重要的支柱。开发一个具有一定规模的程
序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实
现这种方式而诞生的。在浏览器 JavaScript 中,脚本模块的拆分和组合通常使用 HTML 的
script 标签来实现。Node.js 提供了 require 函数来调用其他模块,而且模块都是基于
文件的,机制十分简单。
Node.js 的模块和包机制的实现参照了 CommonJS 的标准,但并未完全遵循。不过
两者的区别并不大,一般来说你大可不必担心,只有当你试图制作一个除了支持 Node.js
之外还要支持其他平台的模块或包的时候才需要仔细研究。通常,两者没有直接冲突的
地方。
我们经常把 Node.js 的模块和包相提并论,因为模块和包是没有本质区别的,两个概念
也时常混用。如果要辨析,那么可以把包理解成是实现了某个功能模块的集合,用于发布
和维护。对使用者来说,模块和包的区别是透明的,因此经常不作区分。本节中我们会详
细介绍:
3.3 模块和包 35
什么是模块;
如何创建并加载模块;
如何创建一个包;
如何使用包管理器 ;
3.3.1 什么是模块
模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个
Node.js 文件就是一个模块,这个文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。
在前面章节的例子中,我们曾经用到了 var http = require('http'),其中 http
是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。我们通过
require 函数获取了这个模块,然后才能使用其中的对象。
3.3.2 创建及加载模块
介绍了什么是模块之后,下面我们来看看如何创建并加载它们。
1. 创建模块
在 Node.js 中,创建一个模块非常简单,因为一个文件就是一个模块,我们要关注的问
题仅仅在于如何在其他文件中获取这个模块。Node.js 提供了 exports 和 require 两个对
象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获
取模块的 exports 对象。
让我们以一个例子来了解模块。创建一个 module.js 的文件,内容是:
//module.js
var name;
exports.setName = function(thyName) {
name = thyName;
};
exports.sayHello = function() {
console.log('Hello ' + name);
};
在同一目录下创建 getmodule.js,内容是:
//getmodule.js
var myModule = require('./module');
36 第 3 章 Node.js 快速入门
myModule.setName('BYVoid');
myModule.sayHello();
运行node getmodule.js,结果是:
Hello BYVoid
在以上示例中,module.js 通过 exports 对象把 setName 和 sayHello 作为模块的访
问接口,在 getmodule.js 中通过 require('./module') 加载这个模块,然后就可以直接访
问 module.js 中 exports 对象的成员函数了。
这种接口封装方式比许多语言要简洁得多,同时也不失优雅,未引入违反语义的特性,
符合传统的编程逻辑。在这个基础上,我们可以构建大型的应用程序,npm 提供的上万个模
块都是通过这种简单的方式搭建起来的。
2. 单次加载
上面这个例子有点类似于创建一个对象,但实际上和对象又有本质的区别,因为
require 不会重复加载模块,也就是说无论调用多少次 require,获得的模块都是同一个。
我们在 getmodule.js 的基础上稍作修改:
//loadmodule.js
var hello1 = require('./module');
hello1.setName('BYVoid');
var hello2 = require('./module');
hello2.setName('BYVoid 2');
hello1.sayHello();
运行后发现输出结果是 Hello BYVoid 2,这是因为变量 hello1 和 hello2 指向的是
同一个实例,因此 hello1.setName 的结果被 hello2.setName 覆盖,最终输出结果是
由后者决定的。
3. 覆盖 exports
有时候我们只是想把一个对象封装到模块中,例如:
//singleobject.js
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
};
exports.Hello = Hello;
此时我们在其他文件中需要通过 require('./singleobject').Hello 来获取
Hello 对象,这略显冗余,可以用下面方法稍微简化:
//hello.js
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello;
这样就可以直接获得这个对象了:
//gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();
注意,模块接口的唯一变化是使用 module.exports = Hello 代替了 exports.Hello=
Hello。在外部引用该模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的
exports。
事实上,exports 本身仅仅是一个普通的空对象,即 {},它专门用来声明接口,本
质上是通过它为模块闭包①的内部建立了一个有限的访问接口。因为它没有任何特殊的地方,
所以可以用其他东西来代替,譬如我们上面例子中的 Hello 对象。
——————————
① 闭包是函数式编程语言的常见特性,具体说明见本书附录A。
38 第 3 章 Node.js 快速入门
不可以通过对 exports 直接赋值代替对 module.exports 赋值。
exports 实际上只是一个和 module.exports 指向同一个对象的变量,
它本身会在模块执行结束后释放,但 module 不会,因此只能通过指定
module.exports 来改变访问接口。
3.3.3 创建包
包是在模块基础上更深一步的抽象,Node.js 的包类似于 C/C++ 的函数库或者 Java/.Net
的类库。它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。Node.js 根
据 CommonJS 规范实现了包机制,开发了 npm来解决包的发布和获取需求。
Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符
合 CommonJS 规范的包应该具备以下特征:
package.json 必须在包的顶层目录下;
二进制文件应该在 bin 目录下;
JavaScript 代码应该在 lib 目录下;
文档应该在 doc 目录下;
单元测试应该在 test 目录下。
Node.js 对包的要求并没有这么严格,只要顶层目录下有 package.json,并符合一些规范
即可。当然为了提高兼容性,我们还是建议你在制作包的时候,严格遵守 CommonJS 规范。
1. 作为文件夹的模块
模块与文件是一一对应的。文件不仅可以是 JavaScript 代码或二进制代码,还可以是一
个文件夹。最简单的包,就是一个作为文件夹的模块。下面我们来看一个例子,建立一个叫
做 somepackage 的文件夹,在其中创建 index.js,内容如下:
//somepackage/index.js
exports.hello = function() {
console.log('Hello.');
};
然后在 somepackage 之外建立 getpackage.js,内容如下:
//getpackage.js
var somePackage = require('./somepackage');
somePackage.hello();
运行 node getpackage.js,控制台将输出结果 Hello.。
我们使用这种方法可以把文件夹封装为一个模块,即所谓的包。包通常是一些模块的集
合,在模块的基础上提供了更高层的抽象,相当于提供了一些固定接口的函数库。通过定制
package.json,我们可以创建更复杂、更完善、更符合规范的包用于发布。
2. package.json
在前面例子中的 somepackage 文件夹下,我们创建一个叫做 package.json 的文件,内容如
下所示:
{
"main" : "./lib/interface.js"
}
然后将 index.js 重命名为 interface.js 并放入 lib 子文件夹下。以同样的方式再次调用这个
包,依然可以正常使用。
Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为
包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 作
为包的接口。
package.json 是 CommonJS 规定的用来描述包的文件,完全符合规范的 package.json 文
件应该含有以下字段。
name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含
空格。
description:包的简要说明。
version:符合语义化版本识别①规范的版本字符串。
keywords:关键字数组,通常用于搜索。
maintainers:维护者数组,每个元素要包含 name、email(可选)、web(可选)
字段。
contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者
数组的第一个元素。
bugs:提交bug的地址,可以是网址或者电子邮件地址。
licenses:许可证数组,每个元素要包含 type (许可证的名称)和 url (链接到
许可证文本的地址)字段。
repositories:仓库托管地址数组,每个元素要包含 type (仓库的类型,如 git )、
url (仓库的地址)和 path (相对于仓库的路径,可选)字段。
——————————
① 语义化版本识别(Semantic Versioning)是由 Gravatars 和 GitHub 创始人 Tom Preston-Werner 提出的一套版本命名
规范,最初目的是解决各式各样版本号大小比较的问题,目前被许多包管理系统所采用。
40 第 3 章 Node.js 快速入门
dependencies:包的依赖,一个关联数组,由包名称和版本号组成。
下面是一个完全符合 CommonJS 规范的 package.json 示例:
{
"name": "mypackage",
"description": "Sample package for CommonJS. This package demonstrates the required
elements of a CommonJS package.",
"version": "0.7.0",
"keywords": [
"package",
"example"
],
"maintainers": [
{
"name": "Bill Smith",
"email": "bills@example.com",
}
],
"contributors": [
{
"name": "BYVoid",
"web": "http://www.byvoid.com/"
}
],
"bugs": {
"mail": "dev@example.com",
"web": "http://www.example.com/bugs"
},
"licenses": [
{
"type": "GPLv2",
"url": "http://www.example.org/licenses/gpl.html"
}
],
"repositories": [
{
"type": "git",
"url": "http://github.com/BYVoid/mypackage.git"
}
],
"dependencies": {
"webkit": "1.2",
"ssl": {
"gnutls": ["1.0", "2.0"],
"openssl": "0.9.8"
}
}
}
3.3.4 Node.js 包管理器
Node.js包管理器,即npm是 Node.js 官方提供的包管理工具①,它已经成了 Node.js 包的
标准发布平台,用于 Node.js 包的发布、传播、依赖控制。npm 提供了命令行工具,使你可
以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包。
1. 获取一个包
使用 npm 安装包的命令格式为:
npm [install/i] [package_name]
例如你要安装 express,可以在命令行运行:
$ npm install express
或者:
$ npm i express
随后你会看到以下安装信息:
npm http GET https://registry.npmjs.org/express
npm http 304 https://registry.npmjs.org/express
npm http GET https://registry.npmjs.org/mime/1.2.4
npm http GET https://registry.npmjs.org/mkdirp/0.3.0
npm http GET https://registry.npmjs.org/qs
npm http GET https://registry.npmjs.org/connect
npm http 200 https://registry.npmjs.org/mime/1.2.4
npm http 200 https://registry.npmjs.org/mkdirp/0.3.0
npm http 200 https://registry.npmjs.org/qs
npm http GET https://registry.npmjs.org/mime/-/mime-1.2.4.tgz
npm http GET https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz
npm http 200 https://registry.npmjs.org/mime/-/mime-1.2.4.tgz
npm http 200 https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz
npm http 200 https://registry.npmjs.org/connect
npm http GET https://registry.npmjs.org/formidable
npm http 200 https://registry.npmjs.org/formidable
express@2.5.8 ./node_modules/express
-- mime@1.2.4
-- mkdirp@0.3.0
-- qs@0.4.2
-- connect@1.8.5
此时 express 就安装成功了,并且放置在当前目录的 node_modules 子目录下。npm 在
——————————
① npm 之于 Node.js,就像 pip 之于 Python,gem 之于 Ruby,pear 之于 PHP,CPAN 之于 Perl ……同时也像 apt-get 之
于 Debian/Ubutnu,yum 之于 Fedora/RHEL/CentOS,homebrew 之于 Mac OS X。
42 第 3 章 Node.js 快速入门
获取 express 的时候还将自动解析其依赖,并获取 express 依赖的 mime、mkdirp、
qs 和 connect。
2. 本地模式和全局模式
npm在默认情况下会从http://npmjs.org搜索或下载包,将包安装到当前目录的node_modules
子目录下。
如果你熟悉 Ruby 的 gem 或者 Python 的 pip,你会发现 npm 与它们的
行为不同,gem 或 pip 总是以全局模式安装,使包可以供所有的程序使用,
而 npm 默认会把包安装到当前目录下。这反映了 npm 不同的设计哲学。如
果把包安装到全局,可以提高程序的重复利用程度,避免同样的内容的多
份副本,但坏处是难以处理不同的版本依赖。如果把包安装到当前目录,
或者说本地,则不会有不同程序依赖不同版本的包的冲突问题,同时还减
轻了包作者的 API 兼容性压力,但缺陷则是同一个包可能会被安装许多次。
在使用 npm 安装包的时候,有两种模式:本地模式和全局模式。默认情况下我们使用 npm
install命令就是采用本地模式,即把包安装到当前目录的 node_modules 子目录下。Node.js
的 require 在加载模块时会尝试搜寻 node_modules 子目录,因此使用 npm 本地模式安装
的包可以直接被引用。
npm 还有另一种不同的安装模式被成为全局模式,使用方法为:
npm [install/i] -g [package_name]
与本地模式的不同之处就在于多了一个参数 -g。我们在 介绍 supervisor那个小节中使用
了 npm install -g supervisor 命令,就是以全局模式安装 supervisor。
为什么要使用全局模式呢?多数时候并不是因为许多程序都有可能用到它,为了减少多
重副本而使用全局模式,而是因为本地模式不会注册 PATH 环境变量。举例说明,我们安装
supervisor 是为了在命令行中运行它,譬如直接运行 supervisor script.js,这时就需要在 PATH
环境变量中注册 supervisor。npm 本地模式仅仅是把包安装到 node_modules 子目录下,其中
的 bin 目录没有包含在 PATH 环境变量中,不能直接在命令行中调用。而当我们使用全局模
式安装时,npm 会将包安装到系统目录,譬如 /usr/local/lib/node_modules/,同时 package.json 文
件中 bin 字段包含的文件会被链接到 /usr/local/bin/。/usr/local/bin/ 是在PATH 环境变量中默认
定义的,因此就可以直接在命令行中运行 supervisor script.js命令了。
使用全局模式安装的包并不能直接在 JavaScript 文件中用 require 获
得,因为 require 不会搜索 /usr/local/lib/node_modules/。我们会在第 6 章
详细介绍模块的加载顺序。
本地模式和全局模式的特点如表3-2所示。
表3-2 本地模式与全局模式
模 式 可通过 require 使用 注册PATH
本地模式 是 否
全局模式 否 是
总而言之,当我们要把某个包作为工程运行时的一部分时,通过本地模式获取,如果要
在命令行下使用,则使用全局模式安装。
在 Linux/Mac 上使用 npm install -g 安装时有可能需要 root 权限,
因为 /usr/local/lib/node_modules/ 通常只有管理员才有权写入。
3. 创建全局链接
npm 提供了一个有趣的命令 npm link,它的功能是在本地包和全局包之间创建符号链
接。我们说过使用全局模式安装的包不能直接通过 require 使用,但通过 npm link命令
可以打破这一限制。举个例子,我们已经通过 npm install -g express 安装了 express,
这时在工程的目录下运行命令:
$ npm link express
./node_modules/express -> /usr/local/lib/node_modules/express
我们可以在 node_modules 子目录中发现一个指向安装到全局的包的符号链接。通过这
种方法,我们就可以把全局包当本地包来使用了。
npm link 命令不支持 Windows。
除了将全局的包链接到本地以外,使用 npm link命令还可以将本地的包链接到全局。
使用方法是在包目录( package.json 所在目录)中运行 npm link 命令。如果我们要开发
一个包,利用这种方法可以非常方便地在不同的工程间进行测试。
4. 包的发布
npm 可以非常方便地发布一个包,比 pip、gem、pear 要简单得多。在发布之前,首先
需要让我们的包符合 npm 的规范,npm 有一套以 CommonJS 为基础包规范,但与 CommonJS
并不完全一致,其主要差别在于必填字段的不同。通过使用 npm init 可以根据交互式问答
产生一个符合标准的 package.json,例如创建一个名为 byvoidmodule 的目录,然后在这个
目录中运行npm init:
44 第 3 章 Node.js 快速入门
$ npm init
Package name: (byvoidmodule) byvoidmodule
Description: A module for learning perpose.
Package version: (0.0.0) 0.0.1
Project homepage: (none) http://www.byvoid.com/
Project git repository: (none)
Author name: BYVoid
Author email: (none) byvoid.kcp@gmail.com
Author url: (none) http://www.byvoid.com/
Main module/entry point: (none)
Test command: (none)
What versions of node does it run on? (~0.6.10)
About to write to /home/byvoid/byvoidmodule/package.json
{
"author": "BYVoid <byvoid.kcp@gmail.com> (http://www.byvoid.com/)",
"name": "byvoidmodule",
"description": "A module for learning perpose.",
"version": "0.0.1",
"homepage": "http://www.byvoid.com/",
"repository": {
"url": ""
},
"engines": {
"node": "~0.6.12"
},
"dependencies": {},
"devDependencies": {}
}
Is this ok? (yes) yes
这样就在 byvoidmodule 目录中生成一个符合 npm 规范的 package.json 文件。创建一个
index.js 作为包的接口,一个简单的包就制作完成了。
在发布前,我们还需要获得一个账号用于今后维护自己的包,使用 npm adduser 根据
提示输入用户名、密码、邮箱,等待账号创建完成。完成后可以使用 npm whoami 测验是
否已经取得了账号。
接下来,在 package.json 所在目录下运行 npm publish,稍等片刻就可以完成发布了。
打开浏览器,访问 http://search.npmjs.org/ 就可以找到自己刚刚发布的包了。现在我们可以在
世界的任意一台计算机上使用 npm install byvoidmodule 命令来安装它。图3-6 是npmjs.
org上包的描述页面。
如果你的包将来有更新,只需要在 package.json 文件中修改 version 字段,然后重新
使用 npm publish 命令就行了。如果你对已发布的包不满意(比如我们发布的这个毫无意
义的包),可以使用 npm unpublish 命令来取消发布。
3.4 调试
写程序时免不了遇到 bug,而当 bug 发生以后,除了抓耳挠腮之外,一个常用的技术是
单步调试。在写 C/C++ 程序的时候,我们有 Visual Studio、gdb 这样顺手的调试器,而脚本
语言开发者就没有这么好的待遇了。多年以来,像 JavaScript 语言一直缺乏有效的调试手段,
“攻城师”只能依靠“眼观六路,耳听八方”的方式进行静态查错,或者在代码之间添加冗
长的输出语句来分析可能出错的地方。直到有了 FireBug、Chrome 开发者工具,JavaScript 才
算有了基本的调试工具。在没有编译器或解译器的支持下,为缺乏内省机制的语言实现一个
调试器是几乎不可能的。Node.js 的调试功能正是由 V8 提供的,保持了一贯的高效和方便的
特性。尽管你也许已经对原始的调试方式十分适应,而且有了一套高效的调试技巧,但我们
还是想介绍一下如何使用 Node.js 内置的工具和第三方模块来进行单步调试。
3.4.1 命令行调试
Node.js 支持命令行下的单步调试。下面是一个简单的程序:
var a = 1;
var b = 'world';
46 第 3 章 Node.js 快速入门
var c = function(x) {
console.log('hello ' + x + a);
};
c(b);
在命令行下执行 node debug debug.js,将会启动调试工具:
< debugger listening on port 5858
connecting... ok
break in /home/byvoid/debug.js:1
1 var a = 1;
2 var b = 'world';
3 var c = function(x) {
debug>
这样就打开了一个 Node.js 的调试终端,我们可以用一些基本的命令进行单步跟踪调试,
参见表3-3。
表3-3 Node.js 调试命令
命 令 功 能
run 执行脚本,在第一行暂停
restart 重新执行脚本
cont, c 继续执行,直到遇到下一个断点
next, n 单步执行
step, s 单步执行并进入函数
out, o 从函数中步出
setBreakpoint(), sb() 在当前行设置断点
setBreakpoint(‘f()’), sb(...) 在函数f的第一行设置断点
setBreakpoint(‘script.js’, 20), sb(...) 在 script.js 的第20行设置断点
clearBreakpoint, cb(...) 清除所有断点
backtrace, bt 显示当前的调用栈
list(5) 显示当前执行到的前后5行代码
watch(expr) 把表达式 expr 加入监视列表
unwatch(expr) 把表达式 expr 从监视列表移除
watchers 显示监视列表中所有的表达式和值
repl 在当前上下文打开即时求值环境
kill 终止当前执行的脚本
scripts 显示当前已加载的所有脚本
version 显示 V8 的版本
下面是一个简单的例子:
$ node debug debug.js
< debugger listening on port 5858
connecting... ok
break in /home/byvoid/debug.js:1
1 var a = 1;
2 var b = 'world';
3 var c = function (x) {
debug> n
break in /home/byvoid/debug.js:2
1 var a = 1;
2 var b = 'world';
3 var c = function (x) {
4 console.log('hello ' + x + a);
debug> sb('debug.js', 4)
1 var a = 1;
2 var b = 'world';
3 var c = function (x) {
* 4 console.log('hello ' + x + a);
5 };
6 c(b);
7 });
debug> c
break in /home/byvoid/debug.js:4
2 var b = 'world';
3 var c = function (x) {
* 4 console.log('hello ' + x + a);
5 };
6 c(b);
debug> repl
Press Ctrl + C to leave debug repl
> x
'world'
> a + 1
2
debug> c
< hello world1
program terminated
3.4.2 远程调试
V8 提供的调试功能是基于 TCP 协议的,因此 Node.js 可以轻松地实现远程调试。在命
令行下使用以下两个语句之一可以打开调试服务器:
node --debug[=port] script.js
node --debug-brk[=port] script.js
48 第 3 章 Node.js 快速入门
node --debug 命令选项可以启动调试服务器,默认情况下调试端口是 5858,也可以
使用 --debug=1234 指定调试端口为 1234。使用 --debug 选项运行脚本时,脚本会正常
执行,但不会暂停,在执行过程中调试客户端可以连接到调试服务器。如果要求脚本暂停执
行等待客户端连接,则应该使用 --debug-brk 选项。这时调试服务器在启动后会立刻暂停
执行脚本,等待调试客户端连接。
当调试服务器启动以后,可以用命令行调试工具作为调试客户端连接,例如:
//在一个终端中
$ node --debug-brk debug.js
debugger listening on port 5858
//在另一个终端中
$ node debug 127.0.0.1:5858
connecting... ok
debug> n
break in /home/byvoid/debug.js:2
1 var a = 1;
2 var b = 'world';
3 var c = function (x) {
4 console.log('hello ' + x + a);
debug>
事实上,当使用 node debug debug.js 命令调试时,只不过是用 Node.js 命令行工
具将以上两步工作自动完成而已。
3.4.3 使用 Eclipse 调试 Node.js
基于 Node.js 的远程调试功能,我们甚至可以用支持 V8 调试协议的 IDE 调试,例如强
大的 Eclipse。Eclipse 是深受广大“码农”喜爱的集成开发环境,有 Java 开发经验的对它一
定不会陌生。在这一小节,我们将会学会如何使用 Eclipse 配置 Node.js 的调试环境,并实现
单步调试功能。
1. 配置调试环境
在使用 Eclipse 之前,首先需要安装 JDK,可以在 http://www.oracle.com/technetwork/java/
javase/downloads/index.html 获得,然后在 http://www.eclipse.org/downloads/ 取得一份 Eclipse。
启动 Eclipse,选择菜单栏中 Help→Install New Software...,此时会打开一个安装对话框,
点击右边的按钮Add...,接下来会打开一个标题为Add Repository的对话框,在 Location 中输
入 http://chromedevtools.googlecode.com/svn/update/dev/,Name 中输入 Chrome Developer,然
后点击OK按钮。参见图3-7、图3-8和图3-9。
图3-7 Help→Install New Software...
图3-8 Add...
图3-9 Add Repository
然后在Work with后面的组合框中选择刚刚添加的Chrome Developer,等待片刻,在列表
中选中Google Chrome Developer Tools,然后点击Next,参见图3-10。
50 第 3 章 Node.js 快速入门
图3-10 Google Chrome Developer Tools
这时 Eclipse 会计算出所需安装的包和依赖,点击Next,参见图3-11。
图3-11 计算依赖
阅读 License,选取I accept the terms of the license agreements,点击Next,参见图3-12。
图3-12 License
接下来 Eclipse 会开始安装,稍等片刻,参见图3-13。
图3-13 安装过程
安装完成以后 Eclipse 会提示重新启动以应用更新,点击Restart Now,V8 调试工具就安
装完成了,参见图3-14。
图3-14 Restart Now
52 第 3 章 Node.js 快速入门
2. 使用 Eclipse 调试 Node.js 程序
用 Eclipse 打开一个 Node.js 代码,选择Debug perspective进入调试视图,如图3-15所示。
点击工具栏中 Debug 图标右边的向下三角形,选择Debug Configurations...(参见图3-16)。在
配置窗口的左侧找到Standalone V8 VM,点击左上角的New图标,会产生一个新的配置。在
配置中填写好Name,如NodeDebug,以及Host和Port。点击Apply应用配置,参见图3-17。
图3-15 Debug perspective
图3-16 Debug Configurations...
图3-17 配置 Standalone V8 VM
接下来,通过 node --debug-brk=5858 debug.js 命令启动要调试脚本的调试服
务器,然后在 Eclipse 的工具栏中点击调试按钮,即可启动调试,如图3-18所示。
图3-18 启动调试
54 第 3 章 Node.js 快速入门
接下来你就可以随心所欲地使用 Eclipse 这个强大的 IDE 来调试 Node.js 脚本了。如果
你对 Eclipse 比较熟悉,你会惊喜地发现 Eclipse 的所有单步调试、断点、监视功能均可以非
常方便地使用。
3.4.4 使用 node-inspector 调试 Node.js
大部分基于 Node.js 的应用都是运行在浏览器中的,例如强大的调试工具 node-inspector。
node-inspector 是一个完全基于 Node.js 的开源在线调试工具,提供了强大的调试功能和友好
的用户界面,它的使用方法十分简便。
首先,使用 npm install -g node-inspector 命令安装 node-inspector,然后在终
端中通过 node --debug-brk=5858 debug.js 命令连接你要除错的脚本的调试服务器,
启动 node-inspector:
$ node-inspector
在浏览器中打开 http://127.0.0.1:8080/debug?port=5858,即可显示出优雅的 Web 调试工
具,参见图3-19。
图3-19 node-inspector
node-inspector 的使用方法十分简单,和浏览器脚本调试工具一样,支持单步、断点、
调用栈帧查看等功能。无论你以前有没有使用过调试工具,都可以在几分钟以内轻松掌握。
node-inspector 使用了 WebKit Web Inspector,因此只能在 Chrome、Safari
等 WebKit 内核的浏览器中使用,而不支持 Firefox 或 Internet Explorer。
3.5 参考资料
《Node Web开发》,David Herron著,人民邮电出版社出版。
node-supervisor: https://github.com/isaacs/node-supervisor。
“Node.js is Cancer”: http://teddziuba.com/2011/10/node-js-is-cancer.html。
“Straight Talk on Event Loops”: http://teddziuba.com/2011/10/straight-talk-on-event- loops.
html。
“nodejs 异步之 Timer & Tick 篇”: http://club.cnodejs.org/topic/4f16442ccae1f4aa2700109b。
“node.js成也异步,败也异步,评node.js的异步特性”: http://www.jiangmiao.org/blog/
2491.html。
“被误解的 Node.js”: https://www.ibm.com/developerworks/cn/web/1201_wangqf_nodejs/。
libev: http://libev.schmorp.de。
“深入浅出Node.js(三):深入Node.js的模块机制”: http://www.infoq.com/cn/articles/
nodejs-module-mechanism。
“npm中本地安装命令行类型的模块是不注册Path的”: http://blog.goddyzhao.me/post/
9835631010/no-direct-command-for-local-installed-command-line-modul。
CommonJS 包/1.0: http://wiki.commonjs.org/wiki/Packages/1.0。
Semantic Versioning 2.0.0-rc.1: http://semver.org/。
“Symlink a package folder – npm”: http://npmjs.org/doc/link.html。
“Publish a package – npm”: http://npmjs.org/doc/publish.html。
“如何在Node.js中使用npm创建和发布一个模块”: http://www.cnblogs.com/piyeyong/
archive/2011/12/ 30/2308153.html。
V8 debugger JSON based protocol: http://code.google.com/p/v8/wiki/DebuggerProtocol。
node-inspector: https://github.com/dannycoates/node-inspector。
Node.js核心模块
第 4 章
58 第 4 章 Node.js 核心模块
核心模块是 Node.js 的心脏,它由一些精简而高效的库组成,为 Node.js 提供了基本的
API。本章中,我们挑选了一部分最常用的核心模块加以详细介绍,主要内容包括:
全局对象;
常用工具;
事件机制;
文件系统访问;
HTTP 服务器与客户端。
4.1 全局对象
JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可
以在程序的任何地方访问,即全局变量。在浏览器 JavaScript 中,通常 window 是全局对象,
而 Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global
对象的属性。
我们在 Node.js 中能够直接访问到对象通常都是 global 的属性,如 console、process
等,下面逐一介绍。
4.1.1 全局对象与全局变量
global 最根本的作用是作为全局变量的宿主。按照 ECMAScript 的定义,满足以下条
件的变量是全局变量:
在最外层定义的变量;
全局对象的属性;
隐式定义的变量(未定义直接赋值的变量)。
当你定义一个全局变量时,这个变量同时也会成为全局对象的属性,反之亦然。需要注
意的是,在 Node.js 中你不可能在最外层定义变量,因为所有用户代码都是属于当前模块的,
而模块本身不是最外层上下文。
永远使用 var 定义变量以避免引入全局变量,因为全局变量会污染
命名空间,提高代码的耦合风险。
4.1.2 process
process 是一个全局变量,即 global 对象的属性。它用于描述当前 Node.js 进程状态
的对象,提供了一个与操作系统的简单接口。通常在你写本地命令行程序的时候,少不了要
和它打交道。下面将会介绍 process 对象的一些最常用的成员方法。
process.argv是命令行参数数组,第一个元素是 node,第二个元素是脚本文件名,
从第三个元素开始每个元素是一个运行参数。
console.log(process.argv);
将以上代码存储为 argv.js,通过以下命令运行:
$ node argv.js 1991 name=byvoid --v "Carbo Kuo"
[ 'node',
'/home/byvoid/argv.js',
'1991',
'name=byvoid',
'--v',
'Carbo Kuo' ]
process.stdout是标准输出流,通常我们使用的 console.log() 向标准输出打印
字符,而 process.stdout.write() 函数提供了更底层的接口。
process.stdin是标准输入流,初始时它是被暂停的,要想从标准输入读取数据,
你必须恢复流,并手动编写流的事件响应函数。
process.stdin.resume();
process.stdin.on('data', function(data) {
process.stdout.write('read from console: ' + data.toString());
});
process.nextTick(callback)的功能是为事件循环设置一项任务,Node.js 会在
下次事件循环调响应时调用 callback。
初学者很可能不理解这个函数的作用,有什么任务不能在当下执行完,需要交给下次事
件循环响应来做呢?我们讨论过,Node.js 适合 I/O 密集型的应用,而不是计算密集型的应用,
因为一个 Node.js 进程只有一个线程,因此在任何时刻都只有一个事件在执行。如果这个事
件占用大量的 CPU 时间,执行事件循环中的下一个事件就需要等待很久,因此 Node.js 的一
个编程原则就是尽量缩短每个事件的执行时间。process.nextTick() 提供了一个这样的
工具,可以把复杂的工作拆散,变成一个个较小的事件。
function doSomething(args, callback) {
somethingComplicated(args);
callback();
}
doSomething(function onEnd() {
compute();
});
60 第 4 章 Node.js 核心模块
我们假设 compute() 和 somethingComplicated() 是两个较为耗时的函数,以上
的程序在调用 doSomething() 时会先执行 somethingComplicated(),然后立即调用
回调函数,在 onEnd() 中又会执行 compute()。下面用 process.nextTick() 改写上
面的程序:
function doSomething(args, callback) {
somethingComplicated(args);
process.nextTick(callback);
}
doSomething(function onEnd() {
compute();
});
改写后的程序会把上面耗时的操作拆分为两个事件,减少每个事件的执行时间,提高事
件响应速度。
不要使用 setTimeout(fn,0)代替 process.nextTick(callback),
前者比后者效率要低得多。
我们探讨了process对象常用的几个成员,除此之外process还展示了process.platform、
process.pid、process.execPath、process.memoryUsage() 等方法,以及 POSIX
进程信号响应机制。有兴趣的读者可以访问 http://nodejs.org/api/process.html 了解详细
内容。
4.1.3 console
console 用于提供控制台标准输出,它是由 Internet Explorer 的 JScript 引擎提供的调试
工具,后来逐渐成为浏览器的事实标准。Node.js 沿用了这个标准,提供与习惯行为一致的
console 对象,用于向标准输出流(stdout)或标准错误流(stderr)输出字符。
console.log():向标准输出流打印字符并以换行符结束。console.log 接受若干
个参数,如果只有一个参数,则输出这个参数的字符串形式。如果有多个参数,则
以类似于 C 语言 printf() 命令的格式输出。第一个参数是一个字符串,如果没有
参数,只打印一个换行。
console.log('Hello world');
console.log('byvoid%diovyb');
console.log('byvoid%diovyb', 1991);
运行结果为:
4.2 常用工具 util 61
Hello world
byvoid%diovyb
byvoid1991iovyb
console.error():与 console.log() 用法相同,只是向标准错误流输出。
console.trace():向标准错误流输出当前的调用栈。
console.trace();
运行结果为:
Trace:
at Object.<anonymous> (/home/byvoid/consoletrace.js:1:71)
at Module._compile (module.js:441:26)
at Object..js (module.js:459:10)
at Module.load (module.js:348:31)
at Function._load (module.js:308:12)
at Array.0 (module.js:479:10)
at EventEmitter._tickCallback (node.js:192:40)
4.2 常用工具 util
util 是一个 Node.js 核心模块,提供常用函数的集合,用于弥补核心 JavaScript 的功能
过于精简的不足。
4.2.1 util.inherits
util.inherits(constructor, superConstructor)是一个实现对象间原型继承
的函数。JavaScript 的面向对象特性是基于原型的,与常见的基于类的不同。JavaScript 没有
提供对象继承的语言级别特性,而是通过原型复制来实现的,具体细节我们在附录A中讨论,
在这里我们只介绍 util.inherits 的用法,示例如下:
var util = require('util');
function Base() {
this.name = 'base';
this.base = 1991;
this.sayHello = function() {
console.log('Hello ' + this.name);
};
}
Base.prototype.showName = function() {
console.log(this.name);
62 第 4 章 Node.js 核心模块
};
function Sub() {
this.name = 'sub';
}
util.inherits(Sub, Base);
var objBase = new Base();
objBase.showName();
objBase.sayHello();
console.log(objBase);
var objSub = new Sub();
objSub.showName();
//objSub.sayHello();
console.log(objSub);
我们定义了一个基础对象 Base 和一个继承自 Base 的 Sub,Base 有三个在构造函数
内定义的属性和一个原型中定义的函数,通过 util.inherits 实现继承。运行结果如下:
base
Hello base
{ name: 'base', base: 1991, sayHello: [Function] }
sub
{ name: 'sub' }
注意,Sub 仅仅继承了 Base 在原型中定义的函数,而构造函数内部创造的 base 属
性和 sayHello 函数都没有被 Sub 继承。同时,在原型中定义的属性不会被 console.log 作
为对象的属性输出。如果我们去掉 objSub.sayHello(); 这行的注释,将会看到:
node.js:201
throw e; // process.nextTick error, or 'error' event on first tick
^
TypeError: Object #<Sub> has no method 'sayHello'
at Object.<anonymous> (/home/byvoid/utilinherits.js:29:8)
at Module._compile (module.js:441:26)
at Object..js (module.js:459:10)
at Module.load (module.js:348:31)
at Function._load (module.js:308:12)
at Array.0 (module.js:479:10)
at EventEmitter._tickCallback (node.js:192:40)
4.2.2 util.inspect
util.inspect(object,[showHidden],[depth],[colors])是一个将任意对象转换
为字符串的方法,通常用于调试和错误输出。它至少接受一个参数 object,即要转换的对象。
4.3 事件驱动 events 63
showHidden 是一个可选参数,如果值为 true,将会输出更多隐藏信息。
depth 表示最大递归的层数,如果对象很复杂,你可以指定层数以控制输出信息的多
少。如果不指定depth,默认会递归2层,指定为 null 表示将不限递归层数完整遍历对象。
如果color 值为 true,输出格式将会以 ANSI 颜色编码,通常用于在终端显示更漂亮
的效果。
特别要指出的是,util.inspect 并不会简单地直接把对象转换为字符串,即使该对
象定义了 toString 方法也不会调用。
var util = require('util');
function Person() {
this.name = 'byvoid';
this.toString = function() {
return this.name;
};
}
var obj = new Person();
console.log(util.inspect(obj));
console.log(util.inspect(obj, true));
运行结果是:
{ name: 'byvoid', toString: [Function] }
{ toString:
{ [Function]
[prototype]: { [constructor]: [Circular] },
[caller]: null,
[length]: 0,
[name]: '',
[arguments]: null },
name: 'byvoid' }
除了以上我们介绍的几个函数之外,util还提供了util.isArray()、util.isRegExp()、
util.isDate()、util.isError() 四个类型测试工具,以及 util.format()、util.
debug() 等工具。有兴趣的读者可以访问 http://nodejs.org/api/util.html 了解详细内容。
4.3 事件驱动 events
events 是 Node.js 最重要的模块,没有“之一”,原因是 Node.js 本身架构就是事件式
的,而它提供了唯一的接口,所以堪称 Node.js 事件编程的基石。events 模块不仅用于用
64 第 4 章 Node.js 核心模块
户代码与 Node.js 下层事件循环的交互,还几乎被所有的模块依赖。
4.3.1 事件发射器
events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就
是事件发射与事件监听器功能的封装。EventEmitter 的每个事件由一个事件名和若干个参
数组成,事件名是一个字符串,通常表达一定的语义。对于每个事件,EventEmitter 支持
若干个事件监听器。当事件发射时,注册到这个事件的事件监听器被依次调用,事件参数作
为回调函数参数传递。
让我们以下面的例子解释这个过程:
var events = require('events');
var emitter = new events.EventEmitter();
emitter.on('someEvent', function(arg1, arg2) {
console.log('listener1', arg1, arg2);
});
emitter.on('someEvent', function(arg1, arg2) {
console.log('listener2', arg1, arg2);
});
emitter.emit('someEvent', 'byvoid', 1991);
运行的结果是:
listener1 byvoid 1991
listener2 byvoid 1991
以上例子中,emitter 为事件 someEvent 注册了两个事件监听器,然后发射了
someEvent 事件。运行结果中可以看到两个事件监听器回调函数被先后调用。
这就是EventEmitter最简单的用法。接下来我们介绍一下EventEmitter常用的API。
EventEmitter.on(event, listener) 为指定事件注册一个监听器,接受一个字
符串 event 和一个回调函数 listener。
EventEmitter.emit(event, [arg1], [arg2], [...]) 发射 event 事件,传
递若干可选参数到事件监听器的参数表。
EventEmitter.once(event, listener) 为指定事件注册一个单次监听器,即
监听器最多只会触发一次,触发后立刻解除该监听器。
EventEmitter.removeListener(event, listener) 移除指定事件的某个监听
器,listener 必须是该事件已经注册过的监听器。
4.4 文件系统 fs 65
EventEmitter.removeAllListeners([event]) 移除所有事件的所有监听器,
如果指定 event,则移除指定事件的所有监听器。
更详细的 API 文档参见 http://nodejs.org/api/events.html。
4.3.2 error 事件
EventEmitter 定义了一个特殊的事件 error,它包含了“错误”的语义,我们在遇到
异常的时候通常会发射 error 事件。当 error 被发射时,EventEmitter 规定如果没有响
应的监听器,Node.js 会把它当作异常,退出程序并打印调用栈。我们一般要为会发射 error
事件的对象设置监听器,避免遇到错误后整个程序崩溃。例如:
var events = require('events');
var emitter = new events.EventEmitter();
emitter.emit('error');
运行时会显示以下错误:
node.js:201
throw e; // process.nextTick error, or 'error' event on first tick
^
Error: Uncaught, unspecified 'error' event.
at EventEmitter.emit (events.js:50:15)
at Object.<anonymous> (/home/byvoid/error.js:5:9)
at Module._compile (module.js:441:26)
at Object..js (module.js:459:10)
at Module.load (module.js:348:31)
at Function._load (module.js:308:12)
at Array.0 (module.js:479:10)
at EventEmitter._tickCallback (node.js:192:40)
4.3.3 继承 EventEmitter
大多数时候我们不会直接使用 EventEmitter,而是在对象中继承它。包括 fs、net、
http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。
为什么要这样做呢?原因有两点。首先,具有某个实体功能的对象实现事件符合语义,
事件的监听和发射应该是一个对象的方法。其次 JavaScript 的对象机制是基于原型的,支持
部分多重继承,继承 EventEmitter 不会打乱对象原有的继承关系。
4.4 文件系统 fs
fs 模块是文件操作的封装,它提供了文件的读取、写入、更名、删除、遍历目录、链
66 第 4 章 Node.js 核心模块
接等 POSIX 文件系统操作。与其他模块不同的是,fs 模块中所有的操作都提供了异步的和
同步的两个版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的
fs.readFileSync()。我们以几个函数为代表,介绍 fs 常用的功能,并列出 fs 所有函数
的定义和功能。
4.4.1 fs.readFile
fs.readFile(filename,[encoding],[callback(err,data)])是最简单的读取
文件的函数。它接受一个必选参数 filename,表示要读取的文件名。第二个参数 encoding
是可选的,表示文件的字符编码。callback 是回调函数,用于接收文件的内容。如果不指
定 encoding,则 callback 就是第二个参数。回调函数提供两个参数 err 和 data,err 表
示有没有错误发生,data 是文件内容。如果指定了 encoding,data 是一个解析后的字符
串,否则 data 将会是以 Buffer 形式表示的二进制数据。
例如以下程序,我们从 content.txt 中读取数据,但不指定编码:
var fs = require('fs');
fs.readFile('content.txt', function(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
假设 content.txt 中的内容是 UTF-8 编码的 Text 文本文件示例,运行结果如下:
<Buffer 54 65 78 74 20 e6 96 87 e6 9c ac e6 96 87 e4 bb b6 e7 a4 ba e4 be 8b>
这个程序以二进制的模式读取了文件的内容,data 的值是 Buffer 对象。如果我们给
fs.readFile 的 encoding 指定编码:
var fs = require('fs');
fs.readFile('content.txt', 'utf-8', function(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
那么运行结果则是:
Text 文本文件示例
当读取文件出现错误时,err 将会是 Error 对象。如果 content.txt 不存在,运行前面
的代码则会出现以下结果:
{ [Error: ENOENT, no such file or directory 'content.txt'] errno: 34, code: 'ENOENT',
path: 'content.txt' }
Node.js 的异步编程接口习惯是以函数的最后一个参数为回调函数,通
常一个函数只有一个回调函数。回调函数是实际参数中第一个是 err,其
余的参数是其他返回的内容。如果没有发生错误,err 的值会是 null 或
undefined。如果有错误发生,err 通常是 Error 对象的实例。
4.4.2 fs.readFileSync
fs.readFileSync(filename, [encoding])是 fs.readFile 同步的版本。它接受
的参数和 fs.readFile 相同,而读取到的文件内容会以函数返回值的形式返回。如果有错
误发生,fs 将会抛出异常,你需要使用 try 和 catch 捕捉并处理异常。
与同步 I/O 函数不同,Node.js 中异步函数大多没有返回值。
4.4.3 fs.open
fs.open(path, flags, [mode], [callback(err, fd)])是 POSIX open 函数的
封装,与 C 语言标准库中的 fopen 函数类似。它接受两个必选参数,path 为文件的路径,
flags 可以是以下值。
r :以读取模式打开文件。
r+ :以读写模式打开文件。
w :以写入模式打开文件,如果文件不存在则创建。
w+ :以读写模式打开文件,如果文件不存在则创建。
a :以追加模式打开文件,如果文件不存在则创建。
a+ :以读取追加模式打开文件,如果文件不存在则创建。
mode 参数用于创建文件时给文件指定权限,默认是 0666①。回调函数将会传递一个文
件描述符 fd②。
——————————
① 文件权限指的是 POSIX 操作系统中对文件读取和访问权限的规范,通常用一个八进制数来表示。例如 0754 表
示文件所有者的权限是 7 (读、写、执行),同组的用户权限是 5 (读、执行),其他用户的权限是 4 (读),
写成字符表示就是 -rwxr-xr--。
② 文件描述符是一个非负整数,表示操作系统内核为当前进程所维护的打开文件的记录表索引。
68 第 4 章 Node.js 核心模块
4.4.4 fs.read
fs.read(fd, buffer, offset, length, position, [callback(err, bytesRead,
buffer)])是 POSIX read 函数的封装,相比 fs.readFile 提供了更底层的接口。fs.read
的功能是从指定的文件描述符 fd 中读取数据并写入 buffer 指向的缓冲区对象。offset 是
buffer 的写入偏移量。length 是要从文件中读取的字节数。position 是文件读取的起始
位置,如果 position 的值为 null,则会从当前文件指针的位置读取。回调函数传递
bytesRead 和 buffer,分别表示读取的字节数和缓冲区对象。
以下是一个使用 fs.open 和 fs.read 的示例。
var fs = require('fs');
fs.open('content.txt', 'r', function(err, fd) {
if (err) {
console.error(err);
return;
}
var buf = new Buffer(8);
fs.read(fd, buf, 0, 8, null, function(err, bytesRead, buffer) {
if (err) {
console.error(err);
return;
}
console.log('bytesRead: ' + bytesRead);
console.log(buffer);
})
});
运行结果则是:
bytesRead: 8
<Buffer 54 65 78 74 20 e6 96 87>
一般来说,除非必要,否则不要使用这种方式读取文件,因为它要求你手动管理缓冲区
和文件指针,尤其是在你不知道文件大小的时候,这将会是一件很麻烦的事情。
表4-1列出了fs所有函数的定义和功能。
表4-1 fs 模块函数表
功 能 异步函数 同步函数
打开文件 fs.open(path,flags, [mode], [callback(err,
fd)])
fs.openSync(path, flags, [mode])
关闭文件 fs.close(fd, [callback(err)]) fs.closeSync(fd)
功 能 异步函数 同步函数
读取文件(文件描
述符)
fs.read(fd,buffer,offset,length,position,
[callback(err, bytesRead, buffer)])
fs.readSync(fd, buffer, offset,
length, position)
写入文件(文件描
述符)
fs.write(fd,buffer,offset,length,position,
[callback(err, bytesWritten, buffer)])
fs.writeSync(fd, buffer, offset,
length, position)
读取文件内容 fs.readFile(filename,[encoding],[callba
ck(err, data)])
fs.readFileSync(filename,
[encoding])
写入文件内容 fs.writeFile(filename, data,[encoding],
[callback(err)])
fs.writeFileSync(filename, data,
[encoding])
删除文件 fs.unlink(path, [callback(err)]) fs.unlinkSync(path)
创建目录 fs.mkdir(path, [mode], [callback(err)]) fs.mkdirSync(path, [mode])
删除目录 fs.rmdir(path, [callback(err)]) fs.rmdirSync(path)
读取目录 fs.readdir(path, [callback(err, files)]) fs.readdirSync(path)
获取真实路径 fs.realpath(path, [callback(err,
resolvedPath)])
fs.realpathSync(path)
更名 fs.rename(path1, path2, [callback(err)]) fs.renameSync(path1, path2)
截断 fs.truncate(fd, len, [callback(err)]) fs.truncateSync(fd, len)
更改所有权 fs.chown(path, uid, gid, [callback(err)]) fs.chownSync(path, uid, gid)
更改所有权(文件
描述符)
fs.fchown(fd, uid, gid, [callback(err)]) fs.fchownSync(fd, uid, gid)
更改所有权(不解
析符号链接)
fs.lchown(path, uid, gid, [callback(err)]) fs.lchownSync(path, uid, gid)
更改权限 fs.chmod(path, mode, [callback(err)]) fs.chmodSync(path, mode)
更改权限(文件描
述符)
fs.fchmod(fd, mode, [callback(err)]) fs.fchmodSync(fd, mode)
更改权限(不解析
符号链接)
fs.lchmod(path, mode, [callback(err)]) fs.lchmodSync(path, mode)
获取文件信息 fs.stat(path, [callback(err, stats)]) fs.statSync(path)
获取文件信息(文
件描述符)
fs.fstat(fd, [callback(err, stats)]) fs.fstatSync(fd)
获取文件信息(不
解析符号链接)
fs.lstat(path, [callback(err, stats)]) fs.lstatSync(path)
创建硬链接 fs.link(srcpath, dstpath, [callback(err)]) fs.linkSync(srcpath, dstpath)
创建符号链接 fs.symlink(linkdata, path, [type],
[callback(err)])
fs.symlinkSync(linkdata, path,
[type])
读取链接 fs.readlink(path, [callback(err,
linkString)])
fs.readlinkSync(path)
修改文件时间戳 fs.utimes(path, atime, mtime, [callback
(err)])
fs.utimesSync(path, atime, mtime)
修改文件时间戳
(文件描述符)
fs.futimes(fd, atime, mtime, [callback
(err)])
fs.futimesSync(fd, atime, mtime)
同步磁盘缓存 fs.fsync(fd, [callback(err)]) fs.fsyncSync(fd)
70 第 4 章 Node.js 核心模块
4.5 HTTP 服务器与客户端
Node.js 标准库提供了 http 模块,其中封装了一个高效的 HTTP 服务器和一个简易的
HTTP 客户端。http.Server 是一个基于事件的 HTTP 服务器,它的核心由 Node.js 下层 C++
部分实现,而接口由 JavaScript 封装,兼顾了高性能与简易性。http.request 则是一个
HTTP 客户端工具,用于向 HTTP 服务器发起请求,例如实现 Pingback①或者内容抓取。
4.5.1 HTTP 服务器
http.Server 是 http 模块中的 HTTP 服务器对象,用 Node.js 做的所有基于 HTTP 协
议的系统,如网站、社交应用甚至代理服务器,都是基于 http.Server 实现的。它提供了
一套封装级别很低的 API,仅仅是流控制和简单的消息解析,所有的高层功能都要通过它的
接口来实现。
我们在 3.1.3 节中使用 http 实现了一个服务器:
//app.js
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>Hello World</p>');
}).listen(3000);
console.log("HTTP server is listening at port 3000.");
这段代码中,http.createServer 创建了一个 http.Server 的实例,将一个函数
作为 HTTP 请求处理函数。这个函数接受两个参数,分别是请求对象( req )和响应对象
( res )。在函数体内,res 显式地写回了响应代码 200 (表示请求成功),指定响应头为
'Content-Type': 'text/html',然后写入响应体 '<h1>Node.js</h1>',通过 res.end
结束并发送。最后该实例还调用了 listen 函数,启动服务器并监听 3000 端口。
1. http.Server 的事件
http.Server 是一个基于事件的 HTTP 服务器,所有的请求都被封装为独立的事件,
开发者只需要对它的事件编写响应函数即可实现 HTTP 服务器的所有功能。它继承自
EventEmitter,提供了以下几个事件。
——————————
① Pingback 是博客系统中用来通知文章被他人引用的一种手段,例如 WordPress 会自动解析文章中的链接,发送
Pingback 以告知链接被引用。
4.5 HTTP 服务器与客户端 71
request:当客户端请求到来时,该事件被触发,提供两个参数 req 和res,分别是
http.ServerRequest 和 http.ServerResponse 的实例,表示请求和响应信息。
connection:当 TCP 连接建立时,该事件被触发,提供一个参数 socket,为
net.Socket 的实例。connection 事件的粒度要大于 request,因为客户端在
Keep-Alive 模式下可能会在同一个连接内发送多次请求。
close :当服务器关闭时,该事件被触发。注意不是在用户连接断开时。
除此之外还有 checkContinue、upgrade、clientError 事件,通常我们不需要关
心,只有在实现复杂的 HTTP 服务器的时候才会用到。
在这些事件中,最常用的就是 request 了,因此 http 提供了一个捷径:
http.createServer([requestListener]) ,功能是创建一个 HTTP 服务器并将
requestListener 作为 request 事件的监听函数,这也是我们前面例子中使用的方法。
事实上它显式的实现方法是:
//httpserver.js
var http = require('http');
var server = new http.Server();
server.on('request', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Node.js</h1>');
res.end('<p>Hello World</p>');
});
server.listen(3000);
console.log("HTTP server is listening at port 3000.");
2. http.ServerRequest
http.ServerRequest 是 HTTP 请求的信息,是后端开发者最关注的内容。它一般由
http.Server 的 request 事件发送,作为第一个参数传递,通常简称 request 或 req。
ServerRequest 提供一些属性,表 4-2 中列出了这些属性。
HTTP 请求一般可以分为两部分:请求头(Request Header)和请求体(Requset Body)。
以上内容由于长度较短都可以在请求头解析完成后立即读取。而请求体可能相对较长,
需要一定的时间传输,因此 http.ServerRequest 提供了以下3个事件用于控制请求体
传输。
data:当请求体数据到来时,该事件被触发。该事件提供一个参数 chunk,表示接
收到的数据。如果该事件没有被监听,那么请求体将会被抛弃。该事件可能会被调
用多次。
72 第 4 章 Node.js 核心模块
end :当请求体数据传输完成时,该事件被触发,此后将不会再有数据到来。
close: 用户当前请求结束时,该事件被触发。不同于 end,如果用户强制终止了
传输,也还是调用close。
表4-2 ServerRequest 的属性
名 称 含 义
complete 客户端请求是否已经发送完成
httpVersion HTTP 协议版本,通常是 1.0 或 1.1
method HTTP 请求方法,如 GET、POST、PUT、DELETE 等
url 原始的请求路径,例如 /static/image/x.jpg 或 /user?name=byvoid
headers HTTP 请求头
trailers HTTP 请求尾(不常见)
connection 当前 HTTP 连接套接字,为 net.Socket 的实例
socket connection 属性的别名
client client 属性的别名
3. 获取 GET 请求内容
注意,http.ServerRequest 提供的属性中没有类似于 PHP 语言中的 $_GET 或
$_POST 的属性,那我们如何接受客户端的表单请求呢?由于 GET 请求直接被嵌入在路径
中,URL是完整的请求路径,包括了 ? 后面的部分,因此你可以手动解析后面的内容作为 GET
请求的参数。Node.js 的 url 模块中的 parse 函数提供了这个功能,例如:
//httpserverrequestget.js
var http = require('http');
var url = require('url');
var util = require('util');
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(util.inspect(url.parse(req.url, true)));
}).listen(3000);
在浏览器中访问 http://127.0.0.1:3000/user?name=byvoid&email=byvoid@byvoid.com,我
们可以看到浏览器返回的结果:
{ search: '?name=byvoid&email=byvoid@byvoid.com',
query: { name: 'byvoid', email: 'byvoid@byvoid.com' },
pathname: '/user',
path: '/user?name=byvoid&email=byvoid@byvoid.com',
href: '/user?name=byvoid&email=byvoid@byvoid.com' }
通过 url.parse①,原始的 path 被解析为一个对象,其中 query 就是我们所谓的 GET
请求的内容,而路径则是 pathname。
4. 获取 POST 请求内容
HTTP 协议 1.1 版本提供了8种标准的请求方法,其中最常见的就是 GET 和 POST。相比
GET 请求把所有的内容编码到访问路径中,POST 请求的内容全部都在请求体中。
http.ServerRequest 并没有一个属性内容为请求体,原因是等待请求体传输可能是一件
耗时的工作,譬如上传文件。而很多时候我们可能并不需要理会请求体的内容,恶意的 POST
请求会大大消耗服务器的资源。所以 Node.js 默认是不会解析请求体的,当你需要的时候,
需要手动来做。让我们看看实现方法:
//httpserverrequestpost.js
var http = require('http');
var querystring = require('querystring');
var util = require('util');
http.createServer(function(req, res) {
var post = '';
req.on('data', function(chunk) {
post += chunk;
});
req.on('end', function() {
post = querystring.parse(post);
res.end(util.inspect(post));
});
}).listen(3000);
上面代码并没有在请求响应函数中向客户端返回信息,而是定义了一个 post 变量,用
于在闭包中暂存请求体的信息。通过 req 的 data 事件监听函数,每当接受到请求体的数据,
就累加到 post 变量中。在 end 事件触发后,通过 querystring.parse 将 post 解析为
真正的 POST 请求格式,然后向客户端返回。
不要在真正的生产应用中使用上面这种简单的方法来获取 POST 请
求,因为它有严重的效率问题和安全问题,这只是一个帮助你理解的示例。
——————————
① url 模块的说明参见 http://nodejs.org/api/url.html。
74 第 4 章 Node.js 核心模块
5. http.ServerResponse
http.ServerResponse 是返回给客户端的信息,决定了用户最终能看到的结果。它
也是由 http.Server 的 request 事件发送的,作为第二个参数传递,一般简称为
response 或 res。
http.ServerResponse 有三个重要的成员函数,用于返回响应头、响应内容以及结束
请求。
response.writeHead(statusCode, [headers]):向请求的客户端发送响应头。
statusCode 是 HTTP 状态码,如 200 (请求成功)、404 (未找到)等。headers
是一个类似关联数组的对象,表示响应头的每个属性。该函数在一个请求内最多只
能调用一次,如果不调用,则会自动生成一个响应头。
response.write(data, [encoding]):向请求的客户端发送响应内容。data 是
一个 Buffer 或字符串,表示要发送的内容。如果 data 是字符串,那么需要指定
encoding 来说明它的编码方式,默认是 utf-8。在 response.end 调用之前,
response.write 可以被多次调用。
response.end([data], [encoding]):结束响应,告知客户端所有发送已经完
成。当所有要返回的内容发送完毕的时候,该函数 必须 被调用一次。它接受两个可
选参数,意义和 response.write 相同。如果不调用该函数,客户端将永远处于
等待状态。
4.5.2 HTTP 客户端
http 模块提供了两个函数 http.request 和 http.get,功能是作为客户端向 HTTP
服务器发起请求。
http.request(options, callback)发起 HTTP 请求。接受两个参数,option 是
一个类似关联数组的对象,表示请求的参数,callback 是请求的回调函数。option
常用的参数如下所示。
host :请求网站的域名或 IP 地址。
port :请求网站的端口,默认 80。
method :请求方法,默认是 GET。
path :请求的相对于根的路径,默认是“/”。QueryString 应该包含在其中。
例如 /search?query=byvoid。
headers :一个关联数组对象,为请求头的内容。
callback 传递一个参数,为 http.ClientResponse 的实例。
http.request 返回一个 http.ClientRequest 的实例。
下面是一个通过 http.request 发送 POST 请求的代码:
//httprequest.js
var http = require('http');
var querystring = require('querystring');
var contents = querystring.stringify({
name: 'byvoid',
email: 'byvoid@byvoid.com',
address: 'Zijing 2#, Tsinghua University',
});
var options = {
host: 'www.byvoid.com',
path: '/application/node/post.php',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length' : contents.length
}
};
var req = http.request(options, function(res) {
res.setEncoding('utf8');
res.on('data', function (data) {
console.log(data);
});
});
req.write(contents);
req.end();
运行后结果如下:
array(3) {
["name"]=>
string(6) "byvoid"
["email"]=>
string(17) "byvoid@byvoid.com"
["address"]=>
string(30) "Zijing 2#, Tsinghua University"
}
不要忘了通过 req.end() 结束请求,否则服务器将不会收到信息。
76 第 4 章 Node.js 核心模块
http.get(options, callback) http 模块还提供了一个更加简便的方法用于处
理GET请求:http.get。它是 http.request 的简化版,唯一的区别在于http.get
自动将请求方法设为了 GET 请求,同时不需要手动调用 req.end()。
//httpget.js
var http = require('http');
http.get({host: 'www.byvoid.com'}, function(res) {
res.setEncoding('utf8');
res.on('data', function (data) {
console.log(data);
});
});
1. http.ClientRequest
http.ClientRequest 是由 http.request 或 http.get 返回产生的对象,表示一
个已经产生而且正在进行中的 HTTP 请求。它提供一个 response 事件,即 http.request
或 http.get 第二个参数指定的回调函数的绑定对象。我们也可以显式地绑定这个事件的
监听函数:
//httpresponse.js
var http = require('http');
var req = http.get({host: 'www.byvoid.com'});
req.on('response', function(res) {
res.setEncoding('utf8');
res.on('data', function (data) {
console.log(data);
});
});
http.ClientRequest 像 http.ServerResponse 一样也提供了 write 和 end 函
数,用于向服务器发送请求体,通常用于 POST、PUT 等操作。所有写结束以后必须调用 end
函数以通知服务器,否则请求无效。http.ClientRequest 还提供了以下函数。
request.abort():终止正在发送的请求。
request.setTimeout(timeout, [callback]):设置请求超时时间,timeout 为
毫秒数。当请求超时以后,callback 将会被调用。
此外还有request.setNoDelay([noDelay])、request.setSocketKeepAlive
([enable], [initialDelay]) 等函数,具体内容请参见 Node.js 文档。
2. http.ClientResponse
http.ClientResponse 与 http.ServerRequest 相似,提供了三个事件 data、end
和 close,分别在数据到达、传输结束和连接结束时触发,其中 data 事件传递一个参数
chunk,表示接收到的数据。
http.ClientResponse 也提供了一些属性,用于表示请求的结果状态,参见表 4-3。
表4-3 ClientResponse 的属性
名 称 含 义
statusCode HTTP 状态码,如 200、404、500
httpVersion HTTP 协议版本,通常是 1.0 或 1.1
headers HTTP 请求头
trailers HTTP 请求尾(不常见)
http.ClientResponse 还提供了以下几个特殊的函数。
response.setEncoding([encoding]):设置默认的编码,当 data 事件被触发
时,数据将会以 encoding 编码。默认值是 null,即不编码,以 Buffer 的形式存
储。常用编码为 utf8。
response.pause():暂停接收数据和发送事件,方便实现下载功能。
response.resume():从暂停的状态中恢复。
4.6 参考资料
Node.js Manual & Documentation: http://nodejs.org/api/index.html。
“Understanding process.nextTick()”: http://howtonode.org/understanding- processnext-tick。
“揭秘Node.js事件”: http://www.grati.org/?p=318。
5.1 准备工作 79
使用Node.js进行Web开发
第 5 章
80 第 5 章 使用 Node.js 进行 Web 开发
阅读到这一章为止,你已经学习了许多知识,但还缺乏实战性的内容。本章,我们打算
从零开始用 Node.js 实现一个微博系统,功能包括路由控制、页面模板、数据库访问、用户
注册、登录、用户会话等内容。
我们会介绍 Express 框架、MVC 设计模式、ejs 模板引擎以及 MongoDB 数据库的操作。
通过实战演练,你将会了解到网站开发的基本方法。本章涉及的代码较多,所有的代码均可
以在www.byvoid.com/project/node找到,但你最好还是亲自输入这些代码。现在就让我们开
始一起动手来实现一个微博网站吧。
5.1 准备工作
在开始动手之前,我们首先要大致知道 Node.js 实现网站的工作原理。Node.js 和 PHP、
Perl、ASP、JSP 一样,目的都是实现动态网页,也就是说由服务器动态生成 HTML 页面。
之所以要这么做,是因为静态 HTML 的可扩展性非常有限,无法与用户有效交互。同时如
果有大量相似的内容,例如产品介绍页面,那么1000个产品就要1000个静态的 HTML 页面,
维护这1000个页面简直是一场灾难,因此动态生成 HTML 页面的技术应运而生。
最早实现动态网页的方法是使用Perl ①和 CGI。在 Perl 程序中输出 HTML 内容,由 HTTP
服务器调用 Perl 程序,将结果返回给客户端。这种方式在互联网刚刚兴起的 20 世纪 90 年代
非常流行,几乎所有的动态网页都是这么做的。但问题在于如果 HTML 内容比较多,维护
非常不方便。大概在 2000 年左右,以 ASP、PHP、JSP 的为代表的以模板为基础的语言出现
了,这种语言的使用方法与 CGI 相反,是在以 HTML 为主的模板中插入程序代码②。这种方
式在2002年前后非常流行,但它的问题是页面和程序逻辑紧密耦合,任何一个网站规模变大
以后,都会遇到结构混乱,难以处理的问题。为了解决这种问题,以 MVC 架构为基础的平
台逐渐兴起,著名的 Ruby on Rails、Django、Zend Framework 都是基于 MVC 架构的。
MVC (Model-View-Controller,模型视图控制器)是一种软件的设计模式,它最早是
由 20 世纪 70 年代的 Smalltalk 语言提出的,即把一个复杂的软件工程分解为三个层面:模
型、视图和控制器。
模型是对象及其数据结构的实现,通常包含数据库操作。
视图表示用户界面,在网站中通常就是 HTML 的组织结构。
控制器用于处理用户请求和数据流、复杂模型,将输出传递给视图。
我们称 PHP、ASP、JSP 为“模板为中心的架构”,表 5-1 是两种Web开发架构的一个
对比。
——————————
① 是C++,任何语言都可以,Perl只是最常见的。
② 例如 ASP 的 <% %> 和 PHP 的 <?php ?> 标签,在这些标签内添加处理代码。
表5-1 Web 开发架构对比
特 性 模板为中心架构 MVC 架构
页面产生方式 执行并替换标签中的语句 由模板引擎生成 HTML 页面
路径解析 对应到文件系统① 由控制器定义
数据访问 通过 SQL 语句查询或访问文件系统 对象关系模型
架构中心 脚本语言是静态 HTTP 服务器的扩展 静态 HTTP 服务器是脚本语言的补充
适用范围 小规模网站 大规模网站
学习难度 容易 较难
这两种架构都出自原始的 CGI,但不同之处是前者走了一条粗放扩张的发展路线,由于
易学易用,在几年前应用较广,而随着互联网规模的扩大,后者优势逐渐体现,目前已经成
为主流。
Node.js 本质上和 Perl 或 C++ 一样,都可以作为 CGI 扩展被调用,但它还可以跳过 HTTP
服务器,因为它本身就是。传统的架构中 HTTP 服务器的角色会由 Apache、Nginx、IIS 之类
的软件来担任,而 Node.js 不需要②。Node.js 提供了 http 模块,它是由 C++ 实现的,性能
可靠,可以直接应用到生产环境。图5-1 是一个简单的架构示意图。
图5-1 Node.js 与 PHP架构的对比
Node.js 和其他的语言相比的另一个显著区别,在于它的原始封装程度较低。例如 PHP 中
你可以访问 $_REQUEST 获取客户端的 POST 或 GET 请求,通常不需要直接处理 HTTP 协
——————————
① 例如 http://example.com/hello/world.php 对应服务器上的 /hello/world.php 这个文件。当然这不是绝对的,现在很多
PHP开发框架都是只提供单个入口,利用服务器的 Rewrite 支持实现了路径的自由控制。我们一般情况下指的是
原生的(或默认的)支持。
② 或者说不是必要的,因为你也可以把 Node.js 的服务器当作 Apache 或 Nginx 后端。
82 第 5 章 使用 Node.js 进行 Web 开发
议①。这些语言要求由 HTTP 服务器来调用,因此你需要设置一个 HTTP 服务器来处理客户
端的请求,HTTP 服务器通过 CGI 或其他方式调用脚本语言解释器,将运行的结果传递回
HTTP 服务器,最终再把内容返回给客户端。而在 Node.js 中,很多工作需要你自己来做(并
不是都要自己动手,因为有第三方框架的帮助)。
5.1.1 使用 http 模块
Node.js 由于不需要另外的 HTTP 服务器,因此减少了一层抽象,给性能带来不少提升,
但同时也因此而提高了开发难度。举例来说,我们要实现一个 POST 数据的表单,例如:
<form method="post" action="http://localhost:3000/">
<input type="text" name="title" />
<textarea name="text"></textarea>
<input type="submit" />
</form>
这个表单包含两个字段:title 和 text,提交时以 POST 的方式将请求发送给
http://localhost:3000/。假设我们要实现的功能是将这两个字段的东西原封不动地返回给用户,
PHP 只需写两行代码,储存为 index.php 放在网站根目录下即可:
echo $_POST['title'];
echo $_POST['text'];
在 3.5.1 节中使用了类似下面的方法(用http模块):
var http = require('http');
var querystring = require('querystring');
var server = http.createServer(function(req, res) {
var post = '';
req.on('data', function(chunk) {
post += chunk;
});
req.on('end', function() {
post = querystring.parse(post);
res.write(post.title);
res.write(post.text);
res.end();
});
}).listen(3000);
——————————
① 比如我们需要知道 HTTP 成功响应时要返回一个 200 状态码,而不需要手动完成“返回 200 状态码”这项工作。
但这不带表你可以轻易地切换到非 HTTP 协议,因为代码仍然是与 HTTP 协议耦合的。
5.1 准备工作 83
这种差别可能会让你大吃一惊,PHP 的实现要比Node.js容易得多。Node.js 完成这样一
个简单任务竟然如此复杂:你需要先创建一个 http 的实例,在其请求处理函数中手动编写
req 对象的事件监听器。当客户端数据到达时,将 POST 数据暂存在闭包的变量中,直到 end
事件触发,解析 POST 请求,处理后返回客户端。
其实这个比较是不公平的,PHP 之所以显得简单并不是因为它没有做这些事,而是因为
PHP 已经将这些工作完全封装好了,只提供了一个高层的接口,而 Node.js 的 http 模块提
供的是底层的接口,尽管使用起来复杂,却可以让我们对 HTTP 协议的理解更加清晰。
但是等等,我们并不是为了理解 HTTP 协议才来使用 Node.js 的,作为 Web 应用开发者,
我们不需要知道实现的细节,更不想与这些细节纠缠从而降低开发效率。难道 Node.js 的抽
象如此之差,把不该有的细节都暴露给了开发者吗?
实际上,Node.js 虽然提供了 http 模块,却不是让你直接用这个模块进行 Web 开发的。
http 模块仅仅是一个 HTTP 服务器内核的封装,你可以用它做任何 HTTP 服务器能做的事
情,不仅仅是做一个网站,甚至实现一个 HTTP 代理服务器都行。你如果想用它直接开发网
站,那么就必须手动实现所有的东西了,小到一个 POST 请求,大到 Cookie、会话的管理。
当你用这种方式建成一个网站的时候,你就几乎已经做好了一个完整的框架了。
5.1.2 Express 框架
npm 提供了大量的第三方模块,其中不乏许多 Web 框架,我们没有必要重复发明轮子,
因而选择使用 Express 作为开发框架,因为它是目前最稳定、使用最广泛,而且 Node.js 官
方推荐的唯一一个 Web 开发框架。
Express ( http://expressjs.com/ ) 除了为 http 模块提供了更高层的接口外,还实现了
许多功能,其中包括:
路由控制;
模板解析支持;
动态视图;
用户会话;
CSRF 保护;
静态文件服务;
错误控制器;
访问日志;
缓存;
插件支持。
需要指出的是,Express 不是一个无所不包的全能框架,像 Rails 或 Django 那样实现了
模板引擎甚至 ORM (Object Relation Model,对象关系模型)。它只是一个轻量级的 Web 框
84 第 5 章 使用 Node.js 进行 Web 开发
架,多数功能只是对 HTTP 协议中常用操作的封装,更多的功能需要插件或者整合其他模块
来完成。
下面用 Express 重新实现前面的例子:
var express = require('express');
var app = express.createServer();
app.use(express.bodyParser());
app.all('/', function(req, res) {
res.send(req.body.title + req.body.text);
});
app.listen(3000);
可以看到,我们不需要手动编写req 的事件监听器了,只需加载express.bodyParser()
就能直接通过 req.body 获取 POST 的数据了。
5.2 快速开始
在上一小节我们已经介绍了 Web 开发的典型架构,我们选择了用 Express 作为开发框架
来开发一个网站,从现在开始我们就要真正动手实践了。
5.2.1 安装 Express
首先我们要安装 Express。如果一个包是某个工程依赖,那么我们需要在工程的目录下
使用本地模式安装这个包,如果要通过命令行调用这个包中的命令,则需要用全局模式安装
(关于本地模式和全局模式,参见 3.3.4节),因此按理说我们使用本地模式安装 Express 即可。
但是Express 像很多框架一样都提供了 Quick Start(快速开始)工具,这个工具的功能通常
是建立一个网站最小的基础框架,在此基础上完成开发。当然你可以完全自己动手,但我还
是推荐使用这个工具更快速地建立网站。为了使用这个工具,我们需要用全局模式安装
Express,因为只有这样我们才能在命令行中使用它。运行以下命令:
$ npm install -g express
等待数秒后安装完成,我们就可以在命令行下通过 express 命令快速创建一个项目了。
在这之前先使用 express --help 查看帮助信息:
Usage: express [options] [path]
Options:
-s, --sessions add session support
-t, --template <engine> add template <engine> support (jade|ejs). default=jade
-c, --css <engine> add stylesheet <engine> support (stylus). default=plain css
-v, --version output framework version
-h, --help output help information
Express 在初始化一个项目的时候需要指定模板引擎,默认支持Jade和ejs,为了降低学
习难度我们推荐使用 ejs ①,同时暂时不添加 CSS 引擎和会话支持。
5.2.2 建立工程
通过以下命令建立网站基本结构:
express -t ejs microblog
当前目录下出现了子目录 microblog,并且产生了一些文件:
create : microblog
create : microblog/package.json
create : microblog/app.js
create : microblog/public
create : microblog/public/javascripts
create : microblog/public/images
create : microblog/public/stylesheets
create : microblog/public/stylesheets/style.css
create : microblog/routes
create : microblog/routes/index.js
create : microblog/views
create : microblog/views/layout.ejs
create : microblog/views/index.ejs
dont forget to install dependencies:
$ cd microblog && npm install
它还提示我们要进入其中运行 npm install,我们依照指示,结果如下:
ejs@0.6.1 ./node_modules/ejs
express@2.5.8 ./node_modules/express
-- qs@0.4.2
-- mime@1.2.4
-- mkdirp@0.3.0
-- connect@1.8.5
它自动安装了依赖 ejs 和 express。这是为什么呢?检查目录中的 package.json 文件,内
容是:
——————————
① ejs (Embedded JavaScript)是一个标签替换引擎,其语法与 ASP、PHP 相似,易于学习,目前被广泛应用。Express
默认提供的引擎是 jade,它颠覆了传统的模板引擎,制定了一套完整的语法用来生成 HTML 的每个标签结构,功
能强大但不易学习。
86 第 5 章 使用 Node.js 进行 Web 开发
{
"name": "microblog"
, "version": "0.0.1"
, "private": true
, "dependencies": {
"express": "2.5.8"
, "ejs": ">= 0.0.1"
}
}
其中 dependencies 属性中有express 和ejs。无参数的 npm install 的功能就是
检查当前目录下的 package.json,并自动安装所有指定的依赖。
5.2.3 启动服务器
用 Express 实现的网站实际上就是一个 Node.js 程序,因此可以直接运行。我们运行 node
app.js,看到 Express server listening on port 3000 in development mode。
接下来,打开浏览器,输入地址 http://localhost:3000,你就可以看到一个简单的 Welcome
to Express 页面了。如果你能看到如图5-2 所示的页面,那么说明你的设定正确无误。
图5-2 Express 初始欢迎页面
要关闭服务器的话,在终端中按 Ctrl + C。注意,如果你对代码做了修改,要想看到修
改后的效果必须重启服务器,也就是说你需要关闭服务器并再次运行才会有效果。如果觉得
有些麻烦,可以使用 supervisor 实现监视代码修改和自动重启,具体使用方法参见 3.1.3 节。
注意命令行中显示服务器运行在开发模式下(development mode),因
此不要在生产环境中部署它。我们会在 6.3 节中介绍如何在真实的生产环
境下部署 Node.js 服务器。
5.2.4 工程的结构
现在让我们回过头来看看 Express 都生成了哪些文件。除了 package.json,它只产生了两
个 JavaScript 文件 app.js 和 routes/index.js。模板引擎 ejs 也有两个文件 index.ejs 和layout.ejs,
此外还有样式表 style.css。下面来详细看看这几个文件。
1. app.js
app.js 是工程的入口,我们先看看其中有什么内容:
/**
* Module dependencies.
*/
var express = require('express')
, routes = require('./routes');
var app = module.exports = express.createServer();
// Configuration
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
app.use(express.errorHandler());
});
// Routes
app.get('/', routes.index);
app.listen(3000);
console.log("Express server listening on port %d in %s mode", app.address().port,
app.settings.env);
88 第 5 章 使用 Node.js 进行 Web 开发
对比上一节使用 Express 的例子,这个文件长了不少,不过并不复杂。下面来分析一下
这段代码。
首先我们导入了 Express 模块,前面已经通过 npm 安装到了本地,在这里可以直接通过
require 获取。routes 是一个文件夹形式的本地模块,即./routes/index.js,它的功能
是为指定路径组织返回内容,相当于 MVC 架构中的控制器。通过 express.createServer()
函数创建了一个应用的实例,后面的所有操作都是针对于这个实例进行的。
接下来是三个 app.configure 函数,分别指定了通用、开发和产品环境下的参数。
第一个 app.configure 直接接受了一个回调函数,后两个则只能在开发和产品环境中调用。
app.set 是 Express 的参数设置工具,接受一个键(key)和一个值(value),可用的参
数如下所示。
basepath:基础地址,通常用于 res.redirect() 跳转。
views:视图文件的目录,存放模板文件。
view engine:视图模板引擎。
view options:全局视图参数对象。
view cache:启用视图缓存。
case sensitive routes:路径区分大小写。
strict routing:严格路径,启用后不会忽略路径末尾的“ / ”。
jsonp callback:开启透明的 JSONP 支持。
Express 依赖于 connect,提供了大量的中间件,可以通过 app.use 启用。app.configure
中启用了5个中间件:bodyParser、methodOverride、router、static以及 errorHandler。
bodyParser 的功能是解析客户端请求,通常是通过 POST 发送的内容。methodOverride
用于支持定制的 HTTP 方法①。router 是项目的路由支持。static 提供了静态文件支持。
errorHandler 是错误控制器。
app.get('/', routes.index); 是一个路由控制器,用户如果访问“ / ”路径,则
由 routes.index 来控制。
最后服务器通过 app.listen(3000); 启动,监听3000端口。
2. routes/index.js
routes/index.js 是路由文件,相当于控制器,用于组织展示的内容:
/*
* GET home page.
*/
exports.index = function(req, res) {
——————————
① 如PUT、DELETE等HTTP方法,浏览器是不支持的。
res.render('index', { title: 'Express' });
};
app.js 中通过 app.get('/', routes.index); 将“ / ”路径映射到 exports.index
函数下。其中只有一个语句 res.render('index', { title: 'Express' }),功能是
调用模板解析引擎,翻译名为 index 的模板,并传入一个对象作为参数,这个对象只有一个
属性,即 title: 'Express'。
3. index.ejs
index.ejs 是模板文件,即 routes/index.js 中调用的模板,内容是:
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
它的基础是 HTML 语言,其中包含了形如 <%= title %> 的标签,功能是显示引用的
变量,即 res.render 函数第二个参数传入的对象的属性。
4. layout.ejs
模板文件不是孤立展示的,默认情况下所有的模板都继承自 layout.ejs,即 <%- body %>
部分才是独特的内容,其他部分是共有的,可以看作是页面框架。
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<%- body %>
</body>
</html>
转载请注明:可思数据 » Node.js从入门到精通