第三章 Node.js基础

3.1 hello world

console.log('Hello World');

node helloworld.js

3.2 Node.js命令行工具

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 的参数直接执行。

3.3 使用 node 的 REPL 模式

REPL (Read-eval-print loop),即输入—求值—输出循环。

3.4 上脚本文件的扩展名不一定是 .js

3.05 建立http服务器

                                            图3-1 Node.js 与 PHP 的架构

//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 所示的内容,我们会发现它并不像 Hello World 一样结束后立即退出,而是一直等待,直到按下 Ctrl + C 才会结束。这是因为 listen 函数中创建了事件监听器,使得 Node.js 进程不会退出事件循环。

3.06 小技巧——使用 supervisor

$ npm install -g 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.

3.07  异步式 I/O 与事件式编程

Node.js 最大的特点就是异步式 I/O(或者非阻塞 I/O)与事件紧密结合的编程模式。这种模式与传统的同步式 I/O 线性的编程思路有很大的不同,因为控制流很大程度上要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元。

3.08 阻塞与线程

什么是阻塞(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 时单线程即可胜任。

                                              图3-3 多线程同步式 I/O

                                                                 图3-4 单线程异步式 I/O

单线程事件驱动的异步式 I/O 比传统的多线程阻塞式 I/O 究竟好在哪里呢?简而言之,异步式 I/O 就是少了多线程的开销。对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。

当然,异步式编程的缺点在于不符合人们一般的程序设计思维,容易让控制流变得晦涩难懂,给编码和调试都带来不小的困难。习惯传统编程模式的开发者在刚刚接触到大规模的异步式应用时往往会无所适从,但慢慢习惯以后会好很多。尽管如此,异步式编程还是较为困难,不过可喜的是现在已经有了不少专门解决异步式编程问题的库(如async)

表3-1 同步式 I/O 和异步式 I/O 的特点

 

3.09 回调函数

在 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.

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.

异步式读取文件就稍微有些违反直觉了,end.先被输出。要想理解结果,我们必须先知道在 Node.js 中,异步式 I/O 是通过回调函数来实现的。fs.readFile 接收了三个参数,第一个是文件名,第二个是编码方式,第三个是一个函数,我们称这个函数为回调函数。JavaScript 支持匿名的函数定义方式,譬如我们例子中回调函数的定义就是嵌套在fs.readFile 的参数表中的。这种定义方式在 JavaScript 程序中极普遍,与下面这种定义方式实现的功能是一致的:

fs.readFile 调用时所做的工作只是将异步式 I/O 请求发送给了操作系统,然后立即返回并执行后面的语句,执行完以后进入事件循环监听事件。当 fs 接收到 I/O 请求完成的事件时,事件循环会主动调用回调函数以完成后续工作。因此我们会先看到 end.,再看到 file.txt 文件的内容。

Node.js 中,并不是所有的 API 都提供了同步和异步版本。Node.js 不鼓励使用同步 I/O。

3.10 事件

Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。

//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 的监听器。

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 不断检查是否有活动的、可供检测的事件监听器,直到检测不到时才退出事件循环,进程结束。

图3-5 事件循环

3.11 模块和包

模块(Module)和包(Package)是 Node.js 最重要的支柱。开发一个具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实现这种方式而诞生的。在浏览器 JavaScript 中,脚本模块的拆分和组合通常使用 HTML 的script 标签来实现。Node.js 提供了 require 函数来调用其他模块,而且模块都是基于文件的,机制十分简单。

模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。

创建模块

在 Node.js 中,创建一个模块非常简单,因为一个文件就是一个模块,我们要关注的问题仅仅在于如何在其他文件中获取这个模块。Node.js 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获取模块的 exports 对象。

//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');
myModule.setName('BYVoid'); 
myModule.sayHello();

运行node getmodule.js,结果是:

Hello BYVoid

在以上示例中,module.js 通过 exports 对象把 setName 和 sayHello 作为模块的访问接口,在 getmodule.js 中通过 require('./module') 加载这个模块,然后就可以直接访问 module.js 中 exports 对象的成员函数了。

单次加载

//loadmodule.js 
var hello1 = require('./module'); 
hello1.setName('BYVoid'); 
var hello2 = require('./module'); 
hello2.setName('BYVoid 2'); 
hello1.sayHello();

覆盖 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 对象。

3.12 创建包

包是在模块基础上更深一步的抽象,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 规范。

 作为文件夹的模块

模块与文件是一一对应的。文件不仅可以是 JavaScript 代码或二进制代码,还可以是一个文件夹。

package.json

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 (相对于仓库的路径,可选)字段。

 dependencies:包的依赖,一个关联数组,由包名称和版本号组成。

3.12 Node.js包管理器

Node.js包管理器,即npm是 Node.js 官方提供的包管理工具①,它已经成了 Node.js 包的标准发布平台,用于 Node.js 包的发布、传播、依赖控制。npm 提供了命令行工具,使你可以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包。

 获取一个包

使用 npm 安装包的命令格式为:

npm [install/i] [package_name]

例如你要安装 express,可以在命令行运行:

$ npm install express
$ npm i express

此时 express 就安装成功了,并且放置在当前目录的 node_modules 子目录下。npm 在获取 express 的时候还将自动解析其依赖,并获取 express 依赖的 mime、mkdirp、qs 和 connect。

本地模式和全局模式

npm在默认情况下会从http://npmjs.org搜索或下载包,将包安装到当前目录的node_modules子目录下。

在使用 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命令了。

表3-2 本地模式与全局模式

创建全局链接

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命令还可以将本地的包链接到全局。使用方法是在包目录( package.json 所在目录)中运行 npm link 命令。如果我们要开发一个包,利用这种方法可以非常方便地在不同的工程间进行测试。

包的发布

npm 可以非常方便地发布一个包,比 pip、gem、pear 要简单得多。在发布之前,首先需要让我们的包符合 npm 的规范,npm 有一套以 CommonJS 为基础包规范,但与 CommonJS 并不完全一致,其主要差别在于必填字段的不同。通过使用 npm init 可以根据交互式问答
产生一个符合标准的 package.json,例如创建一个名为 byvoidmodule 的目录,然后在这个目录中运行npm init:

$ 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) [email protected] 
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 <[email protected]> (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.13 调试

在命令行下执行 node debug debug.js,将会启动调试工具

                                                                                     表3-3 Node.js 调试命令

一个简单的例子:

$ 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

远程调试

V8 提供的调试功能是基于 TCP 协议的,因此 Node.js 可以轻松地实现远程调试。在命令行下使用以下两个语句之一可以打开调试服务器:

node --debug[=port] script.js 
node --debug-brk[=port] script.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 命令行工具将以上两步工作自动完成而已。

今天想了想,还是要积累。不然会慢了很多。

猜你喜欢

转载自blog.csdn.net/boss2967/article/details/82888942