读书笔记: 深入浅出node.js

>> 深入浅出node.js

node.js是c++编写的js运行环境

浏览器: 渲染引擎 + js引擎

后端的js运行环境

node.js用google v8引擎,同时提供很多系统级的API(文件操作 网络编程...)

node.js采用事件驱动 异步编程,为网络服务而设计

浏览器端的js有各种安全限制

node.js提供的多数API都是基于事件的,异步的风格。

node.js的优点:充分利用系统资源,执行代码不会被阻塞以等待某个操作的完成;这个设计非常适合后端的网络服务编程。通过事件注册,回调,可以提高资源的利用率,改善性能。

为方便服务器开发,node.js的网络模块有很多:
HTTP, HTTPS, DNS, NET, UDP, TLS等,有助于快速构建web服务器。

node.js的特点:事件驱动,异步编程,单进程,单线程。
node.js内部通过单线程高效率地维护事件循环队列,没有太多的资源占用和上下文切换。

单核性能出色的node.js如何利用多核CPU呢?建议运行多个node.js进程,用某种通信协议协调各项任务。

javascript的匿名函数和闭包特性非常适合事件驱动,异步编程。

node.js最擅长的事情是与其他服务通信。因为node.js是基于事件的无阻塞的,所以非常适合处理并发请求;缺点:node.js是相对新的一个开源项目,所以不太稳定,一直的变,而且缺少足够多的第三方库支持。

知名项目托管网站GitHub也尝试了node应用(NodeLoad). 使用node.js的项目还有MyFox

Node库socket.io

>> node.js的安装和配置
window版已经编译好的node.js安装包 nodejs.msi,安装后在Node命令行环境输入 node -v,有版本号输出,则说明安装好了。

NPM(Node Package Manager)NodeJs的包管理器。通过NPM我们可以安装Nodejs的第三方库。

Unix/Linux安装NPM:
curl http://npmjs.org/install.sh | sudo sh
获取shell脚本,交给sh命令以sudo权限执行。
安装完之后 输入 npm 回车,如输出帮助信息 则表明安装成功。

用NPM安装第三方库:如underscore
npm install underscore
由于一些特殊的网络环境用npm安装第三方库时,容易卡死;可以通过一个镜像的npm资源库来安装,如:
npm --registry "http://npm.hacknodejs.com/" install underscore

设置为默认npm资源库:
npm config set registry "http://npm.hacknodejs.com" , 设置之后就可以直接 npm install underscore

window下安装NPM:
下载较新的nodejs.msi(v0.10...)安装包并安装后,npm默认也安装了。

命令行下输入 npm回车即可查看npm的帮助信息。

npm install underscore

>> node.js的模块机制
CommonJs规范:构建javascript在包括服务器,桌面,命令行的生态系统。
node.js实现了用require方法引入其他模块,同时npm也基于CommonJs规范定义了包规范,实现了依赖管理和包的自动安装。

> 模块的定义和使用
circle.js:
var PI = Math.PI;
exports.area = function(r){
return PI*r*r
}

exports.length = function(r){
return 2*PI*r
}

app.js:
var circle = require('./circle.js');
console.log('the area of circle of radius 4 is ' + circle.area(4) );

> 模块的载入策略
node.js的模块分为2类:原生(核心)模块 和 文件模块
原生模块在node.js源代码编译时就编译进了二进制执行文件,加载速度最快;文件模块是动态加载的,相对慢。

node.js对require后的原生模块和文件模块都有缓存,第二次require时,从缓存获得,不会有重复的开销。

node.js加载文件模块的工作是有原生模块module来实现和完成的。
文件模块按后缀又分为3类:
.js 通过fs模块同步读取js文件并执行
.node c/c++编写的addon, 通过dlopen方法加载
.json 读取文件 用JSON.parse方法解析。

node.js对app.js这类文件模块的加载过程:
1. 对文件内容进行头尾包装,变为模块定义的形式。如包装后变为:
(function(exports, require, module, __filename, __dirname){
var circle = require('./circle.js');
console.log('the area of circle of radius 4 is ' + circle.area(4) );
})
包装后用vm模块的runInThisContext方法执行这个function对象,并传入对应的实参。

require('./circle.js'); //在这里circle.js模块经历 载入--编译--缓存的过程,最后返回module对象exports。

> require方法的文件查找策略
node.js有原生模块和3种文件模块
先在文件模块缓存区查找--若没有-->看看是否原生模块(是,则到原生模块缓存区查找,没找到则加载之并缓存)--若非原生模块--则查找文件模块----根据后缀 载入文件模块并缓存----最后返回exports

require(..)方法的参数:
1. http, fs, path等原生模块
2. ./mod或../mod,相对路径的文件模块
3. /pathtomodule/mod 绝对路径的文件模块
4. mod 非原生模块的文件模块

module path这个概念:
每一个被加载的文件模块,在创建模块对象时,这个模块便会有一个paths属性,其值是根据当前路径计算得到的路径数组。如:
modulepaths.js:
console.log(module.paths);
把modulepaths.js放到 f:\pan\lm\目录下,在命令行切换到该目录,执行 node modulepaths.js,输出:
[ 'F:\\pan\\lm\\node_modules',
'F:\\pan\\node_modules',
'F:\\node_modules' ]
查找路径生成规则很明显,此外还有一个全局的module path: 当前node执行文件的相对目录 和 环境变量中设置的HOME ,NODE_PATH

简而言之,若require绝对路径的文件,查找时不会遍历每一个node_modules目录,速度最快。

对module path数组的每条路径都执行某个查找过程。

>> 包结构
一个符合CommonJs规范的包结构应该如下:~~node_modules
1. 一个package.json文件应该存在于包的*目录下。
2. 二进制文件应包含在bin目录下
3. javascript代码也应该在bin目录下
4. 文档应该在doc目录下
5. 单元测试应该在test目录下

module paths路径数组中,在每条路径的查找过程中,在尝试添加扩展名也没找到目标文件时,会尝试将当期路径当做包来加载,从package.json中获取信息。读取package.json的main字段。

package.json的字段:
1. name 包名 需要在npm上是唯一的,不能包含空格。
2. description 包简介
3. version 版本号 x.y.z 用于版本控制的场景
4. keywords 关键字数组,用于npm分类搜素
5. maintainers 包维护者的数组 数组元素是形如 {name:..,email:.., web:..}的对象
6. contributors 包贡献者的数组,第一个元素是包作者,形如{name:..,email:..}
7. bugs 一个可以提交bug的url地址
8. license 包所使用的许可证数组 数组元素如{type:...,url:..}
9. repositories 托管源代码的地址数组
10. dependencies 当前包需要的依赖 这个属性很重要,NPM会通过它自动加载依赖包。

包符合CommonJs规范后,就可以发布,输入命令
npm publish <folder>
(需要先注册一个npm账号 : npm adduser)

若用户要使用npm上的包,可以执行:
npm install <package>

本地方式安装包:
从github手动下载包,在命令行下转到包的目录下 执行:
npm install <package.json>

--------------------------------------------------------
node.js中的js模块文件和script标签加载的js文件的区别:node.js的模块文件中声明的变量是在闭包内的,不会污染全局环境,需要被外部调用的接口都挂在exports上。

>> node.js的事件机制
node.js的特点:Evented I/O for V8 javascript (基于V8引擎的事件驱动I/O)

Event模块(events.EventEmitter)是一个简单的事件监听器模式的实现。具有方法:
addListener/on, once, removeListener, removeAllListeners, emit...

node.js的事件与前端Dom树上的事件不同, 不存在冒泡和逐层捕获等行为,自然也没有 preventDefault(), stopPropagation(), stopImmediatePropagation()等处理事件传递的方法

事件侦听器模式也是一种事件钩子(hook)机制,利用事件钩子导出内部数据和状态给外部调用者。

var options = {
host: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST'
};

var req = http.request(options, function(res){
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringfy(res.headers));

res.setEncoding('utf8');
res.on('data', function(chunk){
console.log('BODY:' + chunk);
});

});

req.on('error', function(e){
console.log('problem with request: ' + e.message);
});

//write data to request body
req.write('data\n');
req.write('data2\n');
req.end();

注:如对于一个事件添加超过10个监听器,会得到一条警告,因为设计者认为侦听器太多,可能导致内存泄漏。调用这个语句可以取消10个侦听器的限制: emitter.setMaxListener(0);

若运行时的错误触发了error事件,会交给error侦听器处理,若没有设置error侦听器,则抛出异常,若异常没有被捕获,将会引起退出。

> 如何继承event.EventEmitter类,如node.js中流对象继承EventEmitter类:
//类式继承
function Stream(){
event.EventEmitter.call(this);
}
util.inherits(Stream, event.EventEmitter);

//util.inherits应该是如下这样实现原型链的
util.inherits = function(subClass, superClass){
function F(){}
F.prototype = superClass.prototype;
subClass.prototype = new F;
subClass.prototype.constructor = subClass;
}

> 多事件之间的协作
在大应用中,数据源和web服务器分离是必然的。好处有:相同数据源开发各种丰富的客户端程序,从多个数据源拉取数据,渲染到客户端。
node.js擅长同时并行发起对多个数据源的请求。
并行请求:
api.getUser('username', function(profile){
// got the profile
});

api.getTimeline('username', function(timeline){
// got the timeline
});

api.getSkin('username', function(skin){
// got the skin
});

这里存在一个问题:请求可以并行发出,但是如何控制回调函数的执行顺序?

若改为这样:
api.getUser('username', function(profile){
api.getTimeline('username', function(timeline){
api.getSkin('username', function(skin){
// todo
});
});
});

这将导致请求不能并行发出。

node.js没有原生支持多事件之间协调的方法,需要借助第三方库。如:

var proxy = new EventProxy();
//all方法作用:侦听完事件后,执行回调,并将侦听接收到的参数传入回调中
proxy.all('profile', 'timeline', 'skin', function(profile, timeline, skin){
//todo
});

api.getUser('username', function(profile){
proxy.emit('profile', profile); //触发事件profile,并传入实参profile
});

api.getTimeline('username', function(timeline){
proxy.emit('timeline', timeline);
});

api.getSkin('username', function(skin){
proxy.emit('skin', skin);
});

解决多事件协作的另一种方案:Jscex(代码可以用同步的思维去写,异步方式执行)
如:
var data = $await(Task.whenAll({
profile: api.getUser('username'),
timeline: api.getTimeline('username'),
skin: api.getSkin('username')
}));

//使用:data.profile, data.timeline, data.skin
// todo

> 利用事件队列解决雪崩问题
雪崩问题:指在缓存失效的情景下,大量的并发访问同时涌入数据库查询,数据库无法承受,进而导致网站整体很慢。

看看加一个状态锁的解决方案:
var status = 'ready';
var select = function(callback){
if(status ==='ready'){
status = 'pending'; //上锁
db.select('SQL', function(results){
callback(result);
status = 'ready'; //解锁
});
}
}

连续多次调用select方法时,锁定期间有的select会不被处理;所以应该用事件队列的方式来解决。

var proxy = new EventProxy();
var status = 'ready';
var select = function(callback){
proxy.once('selected', callback);//将所有请求的回调都压入事件队列中
if(status === 'ready'){
status = 'pending';
db.select('SQL', function(result){
proxy.emit('selected', result);
status = 'ready';
});
}
}

>> node.js的异步I/O实现
同步:程序中的后续任务都需要等待I/O的完成,等待的过程中无法充分利用CPU。
实现I/O并行,充分利用CPU的方式有2种:多线程但进程, 单线程多进程

getFile('file_path'); //耗时m
getFileFromNet('url'); //耗时n
同步I/O的话,需要m+n, 异步I/O的话,需要max(m,n)
异步I/O在分布式的环境中很重要,能明显改善性能。

> 异步I/O与轮询技术
进行非阻塞的I/O操作时,要读取到完整数据,应用程序要多次轮询,才能确保数据读取完成,然后进行下一步。轮询技术的缺点是应用程序主动多次调用,占用较多CPU时间片。

>> node.js的异步I/O模型
fs.open = function(path, flags, mode, callback){
callback = arguments[arguments.length - 1];
if( (typeof callback) !== 'function' ){
callback = noop;
}

mode = modeNum(mode, 438); /* 438 = 0666 */
binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, callback);
}

>> string buffer

var fs = require('fs');
var rs = fs.createReadStream('testdata.md');
var data = '';
rs.on('data', function(trunk){
data += trunk; //trunk 是一个buffer对象
});

rs.on('end', function(){
console.log(data);
});

npm install bufferhelper;
bufferconcate.js:
var http = require('http');
var BufferHelper = require('bufferhelper');
http.createServer(function(req, res){
var bufferHelper = new BufferHelper();
req.on('data', function(chunk){
bufferHelper.concat(chunk);
});

req.on('end', function(){
var html = bufferHelper.toBuffer().toString();
res.writeHead(200);
res.end(html);
});
});

>> connect模块(node.js web框架)
中间件的流式处理

var app = connect();

//middleware
app.use(connect.staticCache());
app.use(connect.static(__diranme + '/public') );
app.use(connect.cookieParser());
app.use(connect.session());
...
app.use(function(req, res, next){
//中间件
});

app.listen(3001);

connect用use方法注册中间件到中间件队列中。

上一篇:《深入浅出Node.js》第6章 理解 Buffer


下一篇:一个月时间整理《深入浅出Node.js》