fred-ye / summary

my blog
43 stars 9 forks source link

[NodeJS] 《Node js 开发指南》学习笔记 #53

Open fred-ye opened 8 years ago

fred-ye commented 8 years ago

《Node js 开发指南》学习

标签(空格分隔):Node.js


一些基础的东西

ECMAScript

在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 之外,ActionScript1、 QtScript2、WMLScript3也是 ECMAScript 的实现。

CommonJS 规范与实现

正如当年为了统一 JavaScript 语言标准,人们制定了 ECMAScript 规范一样,如今为了 统一 JavaScript 在浏览器之外的实现,CommonJS 诞生了。CommonJS 试图定义一套普通应 用程序使用的API,从而填补 JavaScript 标准库过于简单的不足。 为了保持中立,CommonJS 不参与标准库实现,其实现交给像 Node.js 之类的项目来完成。 CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、 控制台(console)、编码(encodings)、文件系统(filesystems)、套接字(sockets)、单元测 试(unit testing)等部分。

一个经典的hello world.

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

利用supervisor来监听代码改动时自动重启Node

sudo npm install -g supervisor supervisor的安装需要管理员权限,在项目中我们用的是pm2这个工具。 supervisor app.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.');
//readfilesync.js
var fs = require('fs');
var data = fs.readFileSync('file.txt', 'utf-8'); console.log(data);
console.log('end.');

事件

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);

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

模块和包

模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。 在前面章节的例子中,我们曾经用到了 var http = require('http'),其中 http 是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。我们通过 require 函数获取了这个模块,然后才能使用其中的对象。

创建和加载模块

创建

在 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
var myModule = require('./module');//注意此处是路径
myModule.setName('BYVoid');
myModule.sayHello();

注意: require 不会重复加载模块,也就是说无论调用多少次 require,获得的模块都是同一个

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

创建包

Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符合 CommonJS 规范的包应该具备以下特征:

Node.js 对包的要求并没有这么严格,只要顶层目录下有package.json,并符合一些规范即可。当然为了提高兼容性,我们还是建议你在制作包的时候,严格遵守 CommonJS 规范。

package.json

Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 作 为包的接口。

包管理器

npm是 Node.js 官方提供的包管理工具。 包安装的两种模式:本地模式和全局模式 本地模式: 默认情况下我们使用npm install 命令就是采用本地模式,即把包安装到当前目录的 node_modules 子目录下。Node.js的require在加载模块时会尝试搜寻node_modules子目录, 因此使用npm本地模式安装 的包可以直接被引用。 全局模式: 当我们使用全局模式安装npm install -g时, npm 会将包安装到系统目录,譬如 /usr/local/lib/node_modules/,同时 package.json 文 件中 bin 字段包含的文件会被链接到 /usr/local/bin//usr/local/bin/ 是在PATH 环境变量中默认定义的。

使用全局模式安装的包并不能直接在 JavaScript 文件中用 require 获 得,因为 require 不会搜索 /usr/local/lib/node_modules/.

总而言之,当我们要把某个包作为工程运行时的一部分时,通过本地模式获取,如果要 在命令行下使用,则使用全局模式安装。

调试

通常只用node-inspector

Node.js核心模块

全局对象

全局对象和全局变量

JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。在浏览器 JavaScript 中,通常 window 是全局对象, 而 Node.js 中的全局对象是global, 所有全局变量(除了global本身以外)都是 global对象的属性。

我们在 Node.js 中能够直接访问到对象通常都是global的属性,如 consoleprocess等.

process

process 是一个全局变量,即 global 对象的属性。它用于描述当前 Node.js 进程状态 的对象,提供了一个与操作系统的简单接口。通常在你写本地命令行程序的时候,少不了要和它打交道。

process.argv 是命令行参数数组,第一个元素是 node,第二个元素是脚本文件名, 从第三个元素开始每个元素是一个运行参数

process.stdoutprocess.stdin标准输出输入流

process.nextTick(callback))的功能是为事件循环设置一项任务,Node.js 会在 下次事件循环响应时调用 callback

util.inherits(constructor, superConstructor) 实现对象间原型继承的函数 如

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); 
};
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); 

注意,Sub 仅仅继承了 Base 在原型中定义的函数,而构造函数内部创造的 base 属性和 sayHello 函数都没有被 Sub 继承。同时,在原型中定义的属性不会被 console.log 作 为对象的属性输出。如果我们去掉 objSub.sayHello(); 这行的注释,将会看到 Object #<Sub> has no method 'sayHello'

util.inspect

util.inspect(object,[showHidden],[depth],[colors])是一个将任意对象转换 为字符串的方法,通常用于调试和错误输出。

除了以上我们介绍的几个函数之外,util还提供了util.isArray()、util.isRegExp()、 util.isDate()、util.isError() 四个类型测试工具,以及 util.format()、util. debug() 等工具。有兴趣的读者可以访问 http://nodejs.org/api/util.html 了解详细内容。

事件驱动 events

事件发射器

events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就 是事件发射与事件监听器功能的封装。EventEmitter 的每个事件由一个事件名和若干个参 数组成,事件名是一个字符串,通常表达一定的语义。对于每个事件,EventEmitter 支持 若干个事件监听器。当事件发射时,注册到这个事件的事件监听器被依次调用,事件参数作 为回调函数参数传递。

Demo:

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);

EventEmitter常用API:

EventEmitter 定义了一个特殊的事件 error, 它包含了“错误”的语义,我们在遇到 异常的时候通常会发射error事件。当 error 被发射时, EventEmitter 规定如果没有响应的监听器, Node.js 会把它当作异常,退出程序并打印调用栈。我们一般要为会发射 error 事件的对象设置监听器,避免遇到错误后整个程序崩溃。

如:

var events = require('events');
var emitter = new events.EventEmitter();
emitter.emit('error');

继承 EventEmitter

大多数时候我们不会直接使用 EventEmitter,而是在对象中继承它。包括 fs、net、 http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。 为什么要这样做呢?原因有两点。首先,具有某个实体功能的对象实现事件符合语义, 事件的监听和发射应该是一个对象的方法。其次 JavaScript 的对象机制是基于原型的,支持 部分多重继承,继承 EventEmitter 不会打乱对象原有的继承关系。

注意理解上面一段话。

文件系统 fs

与其他模块不同的是,fs 模块中所有的操作都提供了异步的和 同步的两个版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。

fs.readFile

fs.readFile(filename,[encoding],[callback(err,data)]) 需要注意的是如果指定了encoding, 回调中的data是一个解析后的字符串,否则data将会以Buffer形式表示的二进制数据。

fs.readFileSync

fs.readFileSync(filename, [encoding])fs.readFile 同步的版本。它接受的参数和 fs.readFile 相同,而读取到的文件内容会以函数返回值的形式返回。如果有错 误发生,fs 将会抛出异常,你需要使用 try 和 catch 捕捉并处理异常

fs.open和 fs.read 用得少。

HTTP服务器与客户端

Node.js 标准库提供了 http 模块,其中封装了一个高效的 HTTP 服务器和一个简易的 HTTP 客户端。

HTTP 服务器

http.Server 是 http 模块中的 HTTP 服务器对象,用 Node.js 做的所有基于 HTTP 协议的系统, 如网站、社交应用甚至代理服务器, 都是基于 http.Server实现的。它提供了一套封装级别很低的API,仅仅是流控制和简单的消息解析,所有的高层功能都要通过它的接口来实现。 一个Demo:

//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');

1 http.Server的事件 http.Server 是一个基于事件的 HTTP 服务器, 所有的请求都被封装为独立的事件,开发者只需要对它的事件编写响应函数即可实现 HTTP 服务器的所有功能。它继承自EventEmitter,提供了以下几个事件:

//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。 http.ServerRequest 提供了以下3个事件用于控制请求体传输:

3.获取GET请求内容 url.parse

Demo:

//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);

4. 获取POST请求内容 POST 请求的内容全部都在请求体中。 http.ServerRequest并没有一个属性内容为请求体,原因是等待请求体传输可能是一件 耗时的工作,譬如上传文件。而很多时候我们可能并不需要理会请求体的内容,恶意的 POST 请求会大大消耗服务器的资源。所以Node.js 默认是不会解析请求体的,当你需要的时候, 需要手动来做。 一个Demo

//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 请求格式,然后向客户端返回。

5. http.ServerResponse http.ServerResponse 是返回给客户端的信息,决定了用户最终能看到的结果。它 也是由 http.Server 的 request 事件发送的,作为第二个参数传递,一般简称为 response 或 res。http.ServerResponse 有三个重要的成员函数,用于返回响应头、响应内容以及结束请求。

http 模块提供了两个函数 http.request 和http.get,功能是作为客户端向 HTTP 服务器发起请求。

var http = require('http');
http.get({host: 'www.byvoid.com'}, function(res) {     
    res.setEncoding('utf8');
    res.on('data', function (data) {
         console.log(data);
    });
});

//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 还提供了以下函数。

Express支持同一路径绑定多个路由响应函数,如:

app.all('/user/:username', function(req, res) {
    res.send('all methods captured');
}
app.get('/user/:username', function(req, res) {
    res.send('user: ' + req.params.username);
});

但当你访问任何被这两条同样的规则匹配到的路径时,会发现请求总是被前一条路由规 则捕获,后面的规则会被忽略。原因是 Express 在处理路由规则时,会优先匹配先定义的路由规则,因此后面相同的规则被屏蔽。 Express 提供了路由控制权转移的方法,即回调函数的第三个参数next,通过调用 next(),会将路由控制权转移给后面的规则,例如:

app.all('/user/:username', function(req, res, next) {         
    console.log('all methods captured');
    next();
});
app.get('/user/:username', function(req, res) {
    res.send('user: ' + req.params.username);
});

当访问被匹配到的路径时,如 http://localhost:3000/user/carbo,会发现终端中打印了 all methods captured,而且浏览器中显示了 user: carbo。这说明请求先被第一条路由规 则捕获, 完成console.log后使用next() 转移控制权,又被第二条规则捕获,向浏览器返回了信息.

Node.js进阶

模块加载

Node.js 的模块可以分为两大类,一类是核心模块,另一类是文件模块。核心模块就是 Node.js 标准 API 中提供的模块,如 fs、http、net、vm 等,这些都是由 Node.js 官方提供 的模块,编译成了二进制代码。我们可以直接通过 require 获取核心模块,例如 require('fs')。核心模块拥有最高的加载优先级,换言之如果有模块与其命名冲突, Node.js 总是会加载核心模块。

文件模块则是存储为单独的文件(或文件夹)的模块,可能是 JavaScript 代码、JSON 或 编译好的 C/C++ 代码。文件模块的加载方法相对复杂,但十分灵活,尤其是和 npm 结合使 用时。在不显式指定文件模块扩展名的时候,Node.js 会分别试图加上 .js、.json 和 .node扩展 名。.js 是 JavaScript 代码,.json 是 JSON 格式的文本,.node 是编译好的 C/C++ 代码。 加载顺序是 .js --> .json --> .node

按路径加载

文件模块的加载有两种方式,一种是按路径加载,一种是查找 node_modules 文件夹。如果 require 参数以“ / ”开头, 那么就以绝对路径的方式查找模块名称,例如 require ('/home/byvoid/module') 将会按照优先级依次尝试加载 /home/byvoid/module.js/home/byvoid/module.json/home/byvoid/module.node

如果 require 参数以" ./ "" ../"开头, 那么则以相对路径的方式来查找模块,这种方式在应用中是最常见的

通过查找node_modules目录加载

如果require参数不以"/ "" ./ "" ../ "开头, 而该模块又不是核心模块,那么就 要通过查找 node_modules 加载模块了。我们使用npm获取的包通常就是以这种方式加载的。

控制流问题

循环的陷阱

var fs = require('fs');
var files = ['a.txt', 'b.txt', 'c.txt'];
for (var i = 0; i < files.length; i++) { 
    (function(i) {
        fs.readFile(files[i], 'utf-8', function(err, contents) {             console.log(files[i] + ': ' + contents);
        }); 
    })(i);
}

另一种写法

var fs = require('fs');
var files = ['a.txt', 'b.txt', 'c.txt'];
files.forEach(function(filename) {
    fs.readFile(filename, 'utf-8', function(err, contents) {
        console.log(filename + ': ' + contents);
    });
});

Node.js应用部署

1. 日志功能

Express支持两种模式,开发模式和产品模式,前者的目的是利于调试,后者则是利于部署。使用产品模式运行服务器的方式很简单,只需设置NODE_ENV环境变量。通过 NODE_ENV=production node app.js命令运行服务器可以看到:

Express server listening on port 3000 in production mode

Express 提供了一个访问日志中间件,只需指定stream 参数为一个输出流即可将访问日志写入文件。打开app.js,在最上方加入以下代码:

var fs = require('fs');
var accessLogfile = fs.createWriteStream('access.log', {flags: 'a'}); 
var errorLogfile = fs.createWriteStream('error.log', {flags: 'a'});