NodeJS实战经验总结

看完《Node.js 实战》,整理总结了觉得比较有价值的内容。

1、require

require是少有的同步i/o操作,请只在模块初始化时候使用require。

2、exports与module.exports的区别

最终程序里导出的都是module.exports。
而exports只是对module.exports的一个全局引用,如exports.myFunc为module.exports.myFunc的简写。
为了不破坏exports对module.exports的引用,不能设置exports。
如果破坏了,修复方式:
module.exports = exports = Currency;

3、模块缓存与猴子补丁:

Node 能把模块作为对象缓存起来。
如果程序中的两个文件引入了相同的模块,第一个文件会把模块返回的数据存到程序的内存中,这样第二个文件就不用再去访问和计算模块的源文件了。
实际上在第二个引入是有机会修改缓存数据的。这种方式称为“猴子补丁”(monkey patching ):让一个模块可以改变另一个模块的行为,开发人员可以不用创建它的新版本。

4、Node两种常用的响应逻辑组织方式

一次性为回调函数,绑定的为事件(继承event emitter事件发射器,emit发射消息)

这里给个event emitter的例子:

扩展文件监视器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**Watcher构造器**/
function Watch(watchDir, processDir){
this.watchDir = watchDir;
this.processedDir = processDir;
}
/**继承eventEmitter行为**/
var events = require(‘events’);
util = reuqire(‘util’);
util = util.inherits(Watcher, event.EventEmitter);
/**相当于Watcher.prototype = new events.EventEmitter();**/
/**再增加两个功能**/
var fs = require(‘fs’)
, watchDir = ‘./watch’
, processedDir = ‘./done’
Watcher.prototype.watch = function(){
var watcher = this;
fs.readdir(this.watchDir, function(err, files){
if(err) throw err;
for( var index in files){
watcher.emit(‘process’, files[index]);
}
}
}
Watcher.prototype.start = function(){
var watcher = this;
fs.watchFile(watchDir, function(){
watcher.watch();
});
}

5、减少if/else引起的回调嵌套

有两种方式,可以结合到一起用:

  • (1)嵌套间引入中间函数,通过函数调用拆分嵌套
  • (2)尽早从函数中返回

6、Node的异步回调惯例

Node大多数内置模块使用回调会带两个参数,一个是err或者er,一个是存放结果。

7、进程退出会等待事件异步完成

Node的事件轮询会跟踪还没有完成的异步逻辑,只要有未完成的异步逻辑,Node进程就不会退出。事件轮询会跟踪所有数据库连接,知道它们关闭,以防止Node退出。

8、在Node中使用闭包保留全局变量示例

示例,用闭包私有化color值:
这里有一个asyncFunction函数,对它的调用被封装到一个匿名函数里,参数为color。
这样你就可以马上执行这个匿名函数,把当前的 color 的值传给它。而color 变成了匿名函数内部的本地变量,当 匿名函数外面的color 值发生变化时,本地版的color 不会受影响。

1
2
3
4
5
6
7
8
9
function asyncFunction(callback){
setTimeout(callback, 200);
}
var color = ‘blue’;
(function(color){
asyncFunction(function(){
console.log(’The color is ‘ + color);
})(color);
color = ‘green’;

9、Node的content-length与chunk

Node默认是chunk方式传输(块编码)。当设置content-length时,会隐含禁用Node的块编码。设置content-length传输,数据更少,提升性能。

注意
content-length是字节长度,不是字符长度,一般用Buffer.byteLength(body)

10、__dirname

__dirname表示文件所在目录的路径,在开发时,这个目录和当前工作目录(CWD)是同个目录,但是生产环境可能是从另外一个目录运行。

11、中间件设计惯例

中间件一般有三个参数:请求,响应,回调函数next
为了提供可配置能力,中间件遵循一个惯例:用函数返回另一个函数(闭包)

可配置中间件的基本结构

1
2
3
4
5
function setup(options){
return function(req,res,next){
//中间件逻辑
}
}

使用:

1
app.use(setup({some:’options’}))

12、错误处理中间件

错误处理中间件与普通中间三个参数不同,多了第一个参数err,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function errorHandler(){
var env = process.env.NODE_ENV || ‘development’;
return function(err, req, res, next){
res.statusCode = 500;
switch(env){
case ‘development’:
res.setHeader(‘content-type’, ‘application/json’);
res.end(JSON.stringify(err));
break;
default:
res.end(’Server error’);
}
}
}

当Connect遇到错误,它会跳过后续的中间件,只调用错误处理中间件。

在使用connect时,错误处理有三种方式:

  • (1)使用Connect 默认的错误处理器;
  • (2)自己处理程序错误;
  • (3)使用多个错误处理中间件组件。

第三种给个示例:
多个错误处理器的实现:
api挂载在/api上:

1
2
3
4
5
6
7
8
9
10
var api = connect()
.use(users)
.use(pets)
.use(errorHandler);
var app = connec()
.use(hello)
.use('/api', api)
.use(errorPage)
.listen(3000);

connect_dumplicate_error
其中errorHandler处理来自api的所有错误,error处理来自app的所有错误。

13、connect中间件

包括:

  • (1)cookieParser:为后续中间件提供req.cookies和req.signedCookies,
    设置cookie这样用:res.setHeader(’Set-Cookie’,’’)
  • (2)bodyParser:为后续中间件提供req.body和req.files
  • (3)limit:基于给定字节长度限制请求主体的大小。必须用在bodyParser中间件之前,防止攻击。
    更灵活的使用:
1
2
app.use(type(‘application/x-www-form-urlencoded’, connect.limit(‘64kb’))
app.use(type(‘appication/json’, connect.limit(‘32kb’)))
  • (4)query:为后续中间件提供req.query
  • (5)logger:将http请求的信息输出到stdout或日志文件之类的流中
1
app.user(connect.logger(‘dev’))

输出有颜色区分的日志,便于开发调试
两种输出日志频率
immediate,表示一收到请求,就写日志。
buffer,以毫秒为单位,指定缓冲区刷新的时间间隔。

  • (6)favicon:响应/favicon.ico http请求。通常放在中间件logger前面,这样它就不会出现在你的日志中了
  • (7)methodOverride:可以替不能使用正确请求方法的浏览器仿造req.method,依赖于bodyParser
  • (8)vhost:根据制定的主机名(如nodejs.org)使用给定的中间件和http服务器实例
    可以做反向代理,缺点:一个网站崩溃,他的所有网站都会崩溃,一个都在同一个进程
  • (9)session:为用户设置一个http回话,并提供一个可以跨域请求的持久化req.session对象,依赖于cookieParser
  • (10)basicAuth:为程序提供http基础认证
  • (11)csrf:防止http表单中的跨站请求仿造攻击,依赖于session
  • (12)errorHandler:当出现错误时把堆栈跟踪信息返回给客户端。在开发时使用,不要在生产环境中使用
  • (13)static:把制定目录中的文件发给http客户端,跟connect的挂在功能配合得很好
    返回./public目录下的静态资源文件:
1
app.use(connect.static(‘public’));

默认请求/js/test.js,会去.public/js/test.js去查找。
使用带挂载的static

1
app.use(‘/app/files’, connect.static(‘public’));
  • (14)compress:用gzip压缩优化http响应
  • (15)directory:为http客户端提供目录清单服务,基于客户端的accept请求(普通文件,son或html)提供经过优化的结果

14、 Express中两种渲染视图方式

  • (1)在程序中使用app.render()
  • (2)在请求或者响应层用res.render()

14.1 视图的查找设置

1
app.set(‘views’,__dirname+’/views’);

14.2 设置模板引擎

1
2
3
4
5
6
7
8
9
app.set(‘view engine’, ‘jade’);
app.get(‘/‘, function(){
/**假定为.jade**/
res.render(‘index’);
});
app.get(‘/feed’, function(){
/**因为扩展名为.ejs,所以使用EJS**/
res.render(‘rss.ejs’);
});

14.3 视图缓存

默认会开启view cache,模板修改,需要重启生效。

14.4 视图查找

如photos为复数,暗示是一个资源列表。
express_search

15、单元测试与验收测试

有两种形态:测试驱动(TDD)和行为驱动开发(BDD)

  • 单元测试有Node的assert,Mocha,node unit,Vows以及should.js框架
  • 验收测试,Tobi和Soda框架。
    test-modules-summary

15.1 nodeunit:

例子:创建一个目录,每个测试脚本都应该用测试组装exports对象,

1
2
3
4
5
exports.testPony = function(test){
var isPony = true;
test.ok(isPony, ’This is not a pony.’);
test.done();
}

nodeunit会自动给传入它的对象中引入assert模块。
nodeunit提供test.epect验证断言执行数量是否符合预期。

15.2 mocha

只支持串行测试,默认2s的timeout,并行请用vows:
BDD风格:describe,it,before,after,beforeEach,afterEach.
TDD风格:suite,test,setup,teardown替换上述

执行mocha,会执行./test目录下的javascript文件。

  • BDD风格
1
2
3
4
5
6
7
8
9
10
11
12
13
var memdb = require(‘..’);
var assert = require(‘assert’);
describe(‘memdb’, function(){
describe(‘.save(doc)’, function(){
it(’should save the documment’, function(){
var pey = { name: ’Tobi’ );
memdb.save(pet);
var ret = memdb.first({ name: ’Tobi’ });
assert(ret == pet);
}
)
)
  • TDD风格
1
2
3
4
5
6
7
8
module.exports = {
‘memdb’ : {
‘.save(doc)’ : {
’should save the document’ : function(){
}
}
}
}
  • 测试异步
    增加done()
1
2
3
4
5
6
7
8
9
10
describe(‘.save(doc)’, function(){
it(’should save the document’, function(done){
var pey = { name: ’Tobi’};
memdb.save(pet, function(){
var ret = memdb.first({ name: ’Tobi’ });
assert(ret == pet);
done();
}
}
}

15.3 vows

支持并行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var vows = require(‘vows’);
var assert = require(‘assert’);
var Todo = require(‘./todo’);
vows.describe(’Todo’).addBatch({
‘when adding an item’:{
topic: function(){
var todo = new Todo();
todo.add(‘Feed my cat’);
return todo;
}
},
‘it should exit in my todos’: function(er, todo){
assert.equal(todo.getCount(), 1);
}
}).run();

如果你想把这段代码放到测试文件夹下,放在可以由Vows测试运行期运行,
run.()改成export(module);
然后
vows test/*

15.4 should.js

断言库,它有一个Object .prototype属性:可以写表达能力很强的断言。

15.5 Tobi和Soda

Tobi模拟浏览器测试,可以结合should.js
Soda远程控制真实的浏览器:
test_tobi

16、使用EJS过滤器处理模板数据

格式

<%=:用在转义的EJS输出上的过滤器
<%-:用在非转义的EJS输出上的过滤器

例子

1
2
3
4
5
6
7
var ejs = require(‘ejs’);
var template = ‘<%=: movies | sort | first %>’;
var context = {‘movies’: [
‘Bambi’,
‘Babe: Pig in the city’,
‘Enter the Void'
]};

看来上就是linux的管道符处理,

各种常用处理

  • (1)处理选择:last,first,get:N
  • (2)处理大小写:capitalize把第一个字母变大写,还有upcase,downcase
  • (3)处理文本:把文本截成一定数量的单词truncate:20,替换replace:’A’,’B’,
    排序sort,sort_by:’name’,其中sort返回的是对象,要返回属性的话:|get:’name’
  • (4)map:不用sort_by,再get。直接用map创建一个包含对象属性的数组,
    map ’name’| sort|

其他模板引擎:
Hotgan:实现mustache语法。
Jade:特点是空格的作用,缩进表示HTML的嵌入关系。

17、fs.watchfile()与fs.watch()

fs.watchfile()与fs.watch()是 Node.js中的两个监测文件API。

  • 比较老的是fs.watchFile,使用轮询的方式检查文件,不断的stat(),比较mtime时间戳.
1
2
3
4
5
6
var fs = require(‘fs’);
fs.watchFile(‘/var/log/system.log’, function(curr, prev){
if(cur.mtime.getTime() !== prev.mtime.getTime()){
console.log(‘“System.log” has been modified’);
}
});
  • 新的api是fs.watch(),根据平台本地的文件修改通知API监测文件,性能更优,但是不如watchFile可靠。在OSX监测目录不会报告参数filename,其他见: 档http://nodejs.org/api/fs.html#fs_caveats

18、Process模块

  • process.argv 存储了Node运行当前脚本时传入的参数
  • process.env 获取或设定环境变量
  • process不是eventEmitter实例,却可以发出exit和uncaughtException事件

注意点:
其中exit是在事件循环(event loop)停止之后才激发的,所以你没有机会在exit事件启动任何异步任务。

18.1 Process的信号处理

UINX有信号的概念,是进程间通信(IPC)的基础形式,它是一组固定的名称,不能传递参数。

信号举例如下

  • SIGUSR1:Node进入它内置的调试器
  • SIGWINCH:调试终端大小,由shell发送
  • SIGINT:ctrl+c,由shell发送,Node默认行为是杀死进程。如果你希望在杀掉服务器前,完成所有连接的处理,可以
1
2
3
4
process.on(’SIGINT’, function(){
console.log(‘Got Ctrl-C!’);
server.close();
});
  • 还有SIGUSR2和SIGKILL等等

19、子进程

在NODE中创建子进程三种

  • 高层api,exec:在回调中创建命令并缓存结果的高层api。
  • 底层api,spawn:将单例命令创建Child-Process对象中的底层API。
  • 内置的特殊IPC通道fork:用内置的IPC通道创建额外Node进程的特殊方法。

三者比较

  • cp.exec():只关心结果,不用从子进程的stdio流中访问数据(IRC协议模块有很多,irc,irc-js等等),结果需要转义,可以用execFile()

  • cp.spawn():返回一个可以交互的ChildProcess对象,允许你跟每个子进程的stdio流交互。(node-cgi范例模块)

  • cp.fork():也返回一个ChildProcess对象,区别是这个API是用IPC通道添加的, 子进程现在有一个child.send(message) 函数,并且用fork() 调用的脚本能够监听process.on(‘message’) 事件。fork出来的子进程可以参与运算。

20、其他推荐的社区模块

  • 表单提交:foridable。
  • redis:hiredis,升级node时候,需要重新编译一下hiredis,npm rebuild hiredis。
  • mongodb:mongoose,使用时有个{safe:ture}选项表明你想让数据库操作在执行回调之前完成。

参考:
《Node.js 实战》:http://book.douban.com/subject/25870705/
《Node.js in action》:http://book.douban.com/subject/6805117/

(转载本站文章请注明作者和出处 Vernon Zheng(郑雪峰) – vernonzheng.com ,请勿用于任何商业用途)