Young87

当前位置:首页 >个人收藏

Node.js从入门到精通


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 应用的健壮性,防止恶意攻击。
图1-1 事件循环
这种异步事件模式的弊端也是显而易见的,因为它不符合开发者的常规线性思路,往往
需要把一个完整的逻辑拆分为一个个事件,增加了开发和调试难度。针对这个问题,Node.js
第三方模块提出了很多解决方案,我们会在第6章中详细讨论。
1.4 Node.js 的性能
1.4.1 Node.js 架构简介
Node.js 用异步式 I/O 和事件驱动代替多线程,带来了可观的性能提升。Node.js 除了使
用 V8 作为JavaScript引擎以外,还使用了高效的 libev 和 libeio 库支持事件驱动和异步式 I/O。
图1-2 是 Node.js 架构的示意图。
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-2 Node.js 的架构
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 相关”分类。
图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-4 是 Windows 95 上的 Internet Explorer 3。
图1-4 Windows 95 上的 Internet Explorer 3 
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 的各种实现。
图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 

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

除特别声明,本站所有文章均为原创,如需转载请以超级链接形式注明出处:SmartCat's Blog

上一篇: 欢迎加入 Apache IoTDB !

下一篇: 卷积神经网络各个部分的理解及其前向传播、反向传播的代码实现

精华推荐