ajax的盛行,使javascript成为前端开发人员的宠儿.现今js已经可以通过nodejs在服务器运行.本文将通过对一段代码的逐渐改进来展示如何让程序员使用javascript快乐得进行非阻塞异步编程.nodejs和mongodb的具体安装和使用方法请参考文章最后的参考文献列表.
Hello,Node !
nodejs是编程就绪的,任何功能都要通过编程实现.这和Tomcat,ApacheHTTP等运行就绪服务不同,后者安装后可以直接运行就可以通过浏览器访问.而nodejs起码要有如下代码.
var http = require('http'); http.createServer(function(req,resp){ resp.end('Hello , Node !'); }).listen(8124); console.log('server startup');
将这些代码保存为hellonode.js然后在命令行执行node hellonode.js.在浏览器通过http://localhost:8124/即可访问该服务,浏览器窗口会显示"Hello , Node !"字样.这段代码的意义很容易理解,开头的require相当于java中的import将http模块引入,后面使用http模块的API构建http服务.javascript回调用来处理收到的请求,并返回字符串给浏览器.
一个稍微复杂的例子
现实环境中,常规得处理http请求的过程大概是这样的.首先,服务器收到请求并从中解析出请求参数,然后使用这些参数进行数据库查询,最后将得到的结果放入模板生成视图响应给用户.伪代码大概如下这个样子.
var http = require('http'); var queryString = require('queryString'); var url = require('url'); var db = {};//初始化数据库 http.createServer(function (req, resp) { //1.解析请求参数 var params = queryString.parse(url.parse(req.url).query); var template = 'result = _'; //2.使用参数查询数据 db.query(params, function (err, result) { //3.在回调中使用查询结果 if (err) throw err; var view = template.replace(/_/, result); //4.返回视图,响应给客户端 resp.end(view); }); }).listen(8124); console.log('server startup');
变得更复杂
可以看到代码(2)部分由于数据库查询在node中是无阻塞的,所以必须将所有后续处理查询结果的代码作为回调来使用,上面代码的处理过程比较简单,但是如果放入真实的应用中,不一定如此,比如模板要根据请求参数从本地文件系统加载.代码就会变为如下样子.
var http = require('http'); var queryString = require('queryString'); var url = require('url'); var fs = require('fs'); var db = {};//初始化数据库 http.createServer(function (req, resp) { //解析请求参数 var params = queryString.parse(url.parse(req.url).query); //使用参数查询数据 db.query(params, function (err, result) { //在回调中使用查询结果 if (err) throw err; fs.readFile(params.path, function (err, template) { if (err) throw err; //填充模板 var view = template.replace(/_/, result); //返回 resp.end(view); }) }); resp.end('Hello , Node !'); }).listen(8124); console.log('server startup');
深层嵌套问题
可以看到由于异步编程所以难免产生了又一层的嵌套回调.可以想想当处理过程更复杂的时候,会产生像如下的深层嵌套问题.
setp1(function (err, result1) { setp2(result1, function (err, result2) { setp3(result2, function (err, result3) { step4(result3, function (err, result4) { //更多的嵌套回调 }) }) }) });
事件模型和Promise
这样代码的编写阅读都非常困难.好在现在有许多编程模型可以解决这个问题,例如事件模型(发布订阅).
//注册事件监听器 server.on('connection', function (stream) { console.log('someone connected!'); }); //当如下代码被调用时上面的注册回调将被调用 server.emit('connection')//发布事件
不过这样有一个问题,可以想象如果将上面的深层嵌套用这种方式编写,需要发布四次事件和注册四次监听器.本来一段内聚的代码会变得相当松散,依然没有根本解决读写理解困难的问题.下面介绍Promise模型.大概可以这样理解,每当异步处理执行时,总是返回一个Promise,该Promise保证在处理完成或发成错误时才调用后续的方法.Promise通常有这样的API.
promise.then(success,failure)
success,failure分别表示promise承诺的处理成功或失败时的回调函数.
上面的深层嵌套用该模型重写即可变为如下形式
step1().then(step2).then(step3).then(step3)
这样就容易理解多了.要使用Promise模型,需要想node中引入支持库,我们使用'q'这个模块.可以使用npm install q进行安装.'q'这个模块提供了很多工具方法来将普通的回调转换为Promise.使用'q'改写最上面的请求处理,伪代码如下:
var http = require('http'); var queryString = require('queryString'); var url = require('url'); var fs = require('fs'); var q = require('q'); var db = {};//初始化数据库 http.createServer(function (req, resp) { //解析请求参数 var params = queryString.parse(url.parse(req.url).query); //q.all将数组中的promise合并 q.all([ //q.ninvoke将原本的回调式调用转化为返回Promise对象 q.ninvoke(db, 'query', params), q.ninvoke(fs, 'readFile', params.path) ]) //当上述两个promise都处理完成时继续调用 .then(function (result) { //返回结果为一个数组,数组中分别存放上面被组合的promise的结果 var model = result[0]; var template = result[1]; var view = template.replace(/_/, model); resp.end(view); }); }).listen(8124); console.log('server startup');
这样看起来就容易理解多了,但问题是内部细节很难懂.好在有'q'这样的库帮帮我们处理.
使用CoffeeScript让代码变得更紧凑
javascript是一门古老的语言,不免语法冗余了点儿,好在现在有coffeescript这类的简单语言可以编译为javascript来运行.使用coffee来改写上面代码,就更加紧凑了.
http.createServer (req, resp)-> param = queryString.parse(url.parse(req.url).query) q.all([ q.ninvoke(db, 'query', params), q.ninvoke(fs, 'readFile', params.path)]) .then((result)-> model = result[0] template = result[1] view = template.replace(/_/, modle) resp.end view) console.log 'server startup'
参考文献