首页 > 代码库 > node.js开发指南读书笔记(1)

node.js开发指南读书笔记(1)

<style></style>
3.1 开始使用Node.js编程

3.1.1 Hello World

将以下源代码保存到helloworld.js文件中

console.log(‘Hello World!‘);console.log(‘%s:%d‘, ‘hello‘, 25);

     找到文件位置,执行node helloworld.js。结果如下:

     

3.1.2 Node.js命令行工具

  输入:node --help可以看到详细的帮助信息。

  

  除了直接运行脚本外,node --help显示的用法中说明了另一种输出hello world方式。

  

  使用node的REPL模式

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

  

  进入REPL模式后,会出现一个">"提示符提示你输入,输入后按回车,Node.js将会解析并执行命令。如果你执行了一个函数,那么REPL还会在下面显示这个函数的返回值,上面例子中的undefined就是console.log的返回值,如果你输入了一个错误指令,REPL会立即显示错误并输出调用栈。在任何时候连续按两次CTRL + C即可退出Node.js的REPL模式。

  node提出REPL在应用开发时会给人带来很大的便利,例如我们可以测试一个包能否正常使用,单独调用应用的某一个模块,执行简单的计算等。

3.1.3 建立HTTP服务器

  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所示的内容。

  

          图3-2 用Node.js实现的Http服务器

  用Node.js实现的最简单的HTTP服务器就这样诞生了。这个程序调用了Node.js提供的http模块,对所有HTTP请求答复同样的内容监听3000端口。在终端中运行这个脚本时,我们会发现它并不像Hello World一样结束后立即退出,而是一直等待,直到按下CTRL + C才会结束。这是因为listen函数中创建了事件监听器,使得Node.js进程不会退出事件循环。

  小技巧——使用supervisor。

  安装:$ npm install -g supervisor

  如果你使用的是linux或Mac,直接键入上面的命令很可能有权限错误。原因是npm需要把supervisor安装到系统目录,需要管理员权限,可以使用sudo npm install -g supervisor命令来安装。

  接下来使用supervisor命令启动app.js

  

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

  当然,异步式编程的缺点在于不符合人们一般的程序设计思维,容易让控制流变得晦涩难懂,给编码和调试都带来不小的困难。

  

3.2.2 回调函数

  让我们看看在Node.js中如何使用异步的方式读取一个文件,下面是一个例子:

// readfile.jsvar fs = require(‘fs‘);fs.readFile(‘file.txt‘, ‘utf-8‘, function(err, data){    if (err)    {        console.error(err);    } else    {        console.log(data);    }});console.log(‘end‘)

  在新建一个文件file.txt,输入hello world!

  运行结果如下:

  D:\001code\0011nodejs>node readfile.js
  end
  hello world!

  Node.js 也提供了同步读取文件的API:

// readfilesync.jsvar fs = require(‘fs‘);var data = http://www.mamicode.com/fs.readFileSync(‘file.txt‘, ‘utf-8‘);console.log(data);console.log(‘end‘);

  运行结果如下:

  D:\001code\0011nodejs>node readfilesync.js
  hello world!

  end

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

3.2.3 事件

  Node.js所有的异步I/O操作在完成时都会发送一个事件到事件队列。在开发者看来,事件由EventEmitter对象提供。前面提到的fs.readFile和http.createServer的回调函数都是通过EventEmitter来实现的。下面我们用一个简单的例子说明EventEmitter的用法:

// event.jsvar 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);

  运行这段代码, 一秒后控制台输出了some_event occured.其原理是event对象注册了事件some_event的一个监听器,然后我们通过setTimeout在1000毫秒以后向event对象发送事件some_event,此时会调用some_event监听器。

Node.js的时间循环机制
  Node.js在什么时候会进入事件循环呢?答案是Node.js程序由事件循环开始,到事件循环结束,所有的逻辑都是事件的回调函数,所以Node.js始终在事件循环中,程序入口就是事件循环第一个事件的回调函数。事件的回调函数在执行的过程中,可能会发出I/O请求或直接发射(emit)事件,执行完毕后再事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束。

  与其他语言不同的是,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不断检查是否有活动的、可供检查的时间监听器,直到检测不到时才退出事件循环,进行结束。事件,执行完毕后再事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束。

  与其他语言不同的是,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.3 模块和包

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.jsvar name;exports.setName = function(thyName){  name = thyName;};exports.sayHello = function(){  console.log(‘Hello ‘ + name);}

  在同一目录下,创建getmodule.js,内容是:

// getmodule.jsvar myModule = require(‘./module‘);myModule.setName(‘BYVoid‘);myModule.sayHello();

  运行 node getmodule.js,结果如下:

  hello BYVoid

 (2)单次加载

/*===============================================# Last modified:2014-08-27 13:55# Filename:loadmodule.js# Description: 单次加载示例=================================================*/var hello1 = require(‘./module‘);hello1.setName(‘BYVoid1‘);var hello2 = require(‘./module‘);hello2.setName(‘BYVoide2‘);hello1.sayHello();

  运行 node loadmodule.js 结果如下:

  Hello BYVoide2

  这是因为变量hello1和hello2指向的是同一个实例,因此hello1.setName的结果被hello2.setName覆盖,最终结果是由后者决定。

  (3)覆盖exports

  a 第一种方法 exports.Hello

  模块

/*===============================================# Author: RollerCoaster# Last modified:2014-08-27 14:02# Filename: singleobject.js# Description: 覆盖exports示例1=================================================*/function Hello(){    var name;    this.setName = function(thyName){        name = thyName;    };    this.sayHello = function(){    console.log(‘Hello ‘ + name + ‘!‘);        };};exports.Hello = Hello;

  调用代码:

/*===============================================# Author: RollerCoaster # Last modified:2014-08-27 14:05# Filename: runSingleObject.js# Description: 调用singleobject模块=================================================*/var Hello = require(‘./singleobject‘).Hello;hello = new Hello();hello.setName(‘roller coaster‘); hello.sayHello();

  运行结果:

  Hello roller coaster!

    b 第二种方法 module.exports = Hello;

  模块代码:

/*===============================================# Author: RollerCoaster# Last modified:2014-08-27 14:17# Filename: hello.js# Description: hello 模块=================================================*/function Hello(){    var name;    this.setName = function(thyName){    name=thyName;    };        this.sayHello = function(){    console.log(‘Hello ‘ + name + ‘!‘);    }};module.exports = Hello;

  调用代码

/*===============================================# Author: RollerCoaster# Last modified:2014-08-27 14:17# Filename: gethello.js# Description: 调用hello模块=================================================*/var Hello = require(‘./Hello‘);hello = new Hello();hello.setName(‘roller coaster‘);hello.sayHello();

  注意,模块接口的唯一变化是使用module.exports = Hello 代替了exports.Hello = Hello。在外部调用该模块时,其接口对象就是要输出Hello对象本身,而不是原先的exports。  

  事实上,exports本身仅仅是一个普通的空对象,即{},它专门用来声明接口,本质上是通过它为模块闭包内部建立了一个有限的访问接口。因为它没有任何特殊的地方。所以可以用其他东西来代替,譬如我们上面例子上的Hello对象。

  不可以通过对 exports直接复制代替对module.exports赋值。exports实际上只是一个和module.exports指向同一个对象的变量,它本身会在模块执行结束后释放,但module不会,因此只能通过指定module.exports来改变访问接口。

 

node.js开发指南读书笔记(1)