Node.js:用JavaScript写服务器端程序-介绍并写个MVC框架[转]

Node.js:用JavaScript写服务器端程序-介绍并写个MVC框架

(注:1、本文基于Node.js V0.3.6; 2、本文假设你了解JavaScript; 3、本文假设你了解MVC框架;4、本文作者:QLeelulu,转载请注明出处。5、本文示例源代码:learnNode.zip)

 

Node.js是什么

Node让你可以用javascript编写服务器端程序,让javascript脱离web浏览器的限制,像C#、JAVA、Python等语言一样在服务器端运行,这也让一些熟悉Javascript的前端开发人员进军到服务器端开发提供了一个便利的途径。 Node是基于Google的V8引擎封装的,并提供了一些编写服务器程序的常用接口,例如文件流的处理。Node的目的是提供一种简单的途径来编写高性能的网络程序。

Node.js的性能

hello world 测试:

clip_image002

300并发请求,返回不同大小的内容:

clip_image004

为什么node有如此高的性能?看node的特性。

Node.js的特性

1. 单线程

2. 非阻塞IO

3. Google V8

4. 事件驱动

更详细的了解node请看淘宝UED博客上的关于node.js的一个幻灯片:http://www.slideshare.net/lijing00333/node-js

你好,世界

这,当然是俗套的Hello World啦(hello_world.js):

var http = require( ' http');
http.createServer( function (req, res) {
res.writeHead( 200 , { ' Content-Type': 'text/plain'});
res.end( ' Hello World\n');
}).listen( 8124 , " 127.0.0.1 " );
console.log( ' Server running at http://127.0.0.1:8124/');

require类似于C#的using、Python的import,用于导入模块(module)。node使用的是CommonJS的模块系统。http.createServer 的参数为一个函数,每当有新的请求进来的时候,就会触发这个函数。最后就是绑定要监听的端口。

怎么运行?

当然,是先安装node.js啦。到http://nodejs.org/下载并编译,支持Linux、Mac,也支持windows下的Cygwin。具体的安装说明见:http://howtonode.org/how-to-install-nodejs

装好node后,就可以运行我们的hello world了:

$ node hello_world.js
Server running at http: // 127.0 . 0.1 : 8124 /

 

clip_image006

编程习惯的改变?

我们来写一个读取文件内容的脚本:

 

?
1
2
3
4
5
6
7
8
9
10
11
//output_me.js
var fs = require( 'fs' ), fileContent = 'nothing' ;
fs.readFile(__filename, "utf-8" , function (err, file) { 
     if (err) { 
         console.log(err);
         return
     }
     fileContent = file;
     console.log( 'end readfile \n' );
});
console.log( 'doSomethingWithFile: ' + fileContent + '\n' );

这个脚本读取当前文件的内容并输出。__filename是node的一个全局变量,值为当前文件的绝对路径。我们执行这个脚本看一下:

clip_image008

有没发现结果不对呢?打印的fileContent并不是读取到的文件内容,而是初始化的时候赋值的nothing,并且‘end readfile’最后才打印出来。前面我们提到node的一个特性就是非阻塞IO,而readFile就是异步非阻塞读取文件内容的,所以后面的代码并不会等到文件内容读取完了再执行。请谨记node的异步非阻塞IO特性。所以我们需要将上面的代码修改为如下就能正常工作了:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
//output_me.js
var fs = require( 'fs' ), fileContent = 'nothing' ;
fs.readFile(__filename, "utf-8" , function (err, file) { 
     if (err) { 
         console.log(err);
         return
     }
     fileContent = file;
     //对于file的处理放到回调函数这里处理
     console.log( 'doSomethingWithFile: ' + fileContent + '\n' );
});
console.log( '我们先去喝杯茶\n' );

写个Web MVC框架试试

下面我们用node来写一个小玩具:一个Web MVC框架。这个小玩具我称它为n2Mvc,它的代码结构看起来大概如下:

clip_image010

和hello world一样,我们需要一个http的服务器来处理所有进来的请求:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var http = require( 'http' ),
     querystring = require( "querystring" );
exports.runServer = function (port){
     port = port || 8080;
     var server = http.createServer( function (req, res){
         var _postData = '' ;
         //on用于添加一个监听函数到一个特定的事件
         req.on( 'data' , function (chunk)
         {
             _postData += chunk;
         })
         .on( 'end' , function ()
         {
             req.post = querystring.parse(_postData);
             handlerRequest(req, res);
         });
     }).listen(port);
     console.log( 'Server running at http://127.0.0.1:' + port + '/' );
};

这里定义了一个runServer的方法来启动我们的n2Mvc的服务器。有没注意到runServer前面有个exports?这个exports相当于C#中的publish,在用require导入这个模块的时候,runServer可以被访问到。我们写一个脚本来演示下node的模块导入系统:

 

?
1
2
3
4
5
//moduleExample.js
var myPrivate = '艳照,藏着' ;
exports.myPublish = '冠西的相机' ;
this .myPublish2 = 'this也可以哦' ;
console.log( 'moduleExample.js loaded \n' );

执行结果:

clip_image002[9]

从结果中我们可以看出exports和this下的变量在外部导入模块后,可以被外部访问到,而var定义的变量只能在脚本内部访问。

从结果我们还可以看出,第二次require导入moduleExample模块的时候,并没有打印“moduleExample.js loaded”,因为require导入模块的时候,会先从require.cache 中检查模块是否已经加载,如果没有加载,才会从硬盘中查找模块脚本并加载。

require支持相对路径查找模块,例如上面代码中require('./moduleExample')中的“./”就代表在当前目录下查找。如果不是相当路径,例如 require('http'),node则会到require.paths中去查找,例如我的系统require.paths为:

clip_image004[7]

当require('http')的时候,node的查找路径为:

复制代码
1 、 / home / qleelulu / .node_modules / http
2 、 / home / qleelulu / .node_modules / http.js
3 、 / home / qleelulu / .node_modules / http.node
4 、 / home / qleelulu / .node_modules / http / index.js
5 、 / home / qleelulu / .node_modules / http / index.node
6 、 / home / qleelulu / .node_libraries / http
7 、 / home / qleelulu / .node_libraries / http.js
8 、参考前面
复制代码

再看回前面的代码,http.createServer中的回调函数中的request注册了两个事件,前面提到过node的一个特点是事件驱动的,所以这种事件绑定你会到处看到(想想jQuery的事件绑定?例如$('a').click(fn))。关于node的事件我们在后面再细说。request对象的data事件会在接收客户端post上来的数据时候触发,而end事件则会在最后触发。所以我们在data事件里面处理接收到的数据(例如post过来的form表单数据),在end事件里面通过handlerRequest 函数来统一处理所有的请求并分发给相应的controller action处理。

handlerRequest的代码如下:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var route = require( './route' );
var handlerRequest = function (req, res){
     //通过route来获取controller和action信息
     var actionInfo = route.getActionInfo(req.url, req.method);
     //如果route中有匹配的action,则分发给对应的action
     if (actionInfo.action){
         //假设controller都放到当前目录的controllers目录里面,还记得require是怎么搜索module的么?
         var controller = require( './controllers/' +actionInfo.controller); // ./controllers/blog
         if (controller[actionInfo.action]){
             var ct = new controllerContext(req, res);
             //动态调用,动态语言就是方便啊
             //通过apply将controller的上下文对象传递给action
             controller[actionInfo.action].apply(ct, actionInfo.args);
         } else {
             handler500(req, res, 'Error: controller "' + actionInfo.controller + '" without action "' + actionInfo.action + '"' )
         }
     } else {
         //如果route没有匹配到,则当作静态文件处理
         staticFileServer(req, res);
     }
};

这里导入来一个route模块,route根据请求的url等信息去获取获取controller和action的信息,如果获取到,则通过动态调用调用action方法,如果没有匹配的action信息,则作为静态文件处理。

下面是route模块的代码:

 

?
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
33
34
35
36
37
38
39
40
41
42
var parseURL = require( 'url' ).parse;
//根据http请求的method来分别保存route规则
var routes = {get:[], post:[], head:[], put:[], delete :[]};
/**
* 注册route规则
* 示例:
* route.map({
*     method:'post',
*     url: /\/blog\/post\/(\d+)\/?$/i,
*     controller: 'blog',
*     action: 'showBlogPost'
* })
*/
exports.map = function (dict){
     if (dict && dict.url && dict.controller){
         var method = dict.method ? dict.method.toLowerCase() : 'get' ;
         routes[method].push({
             u: dict.url, //url匹配正则
             c: dict.controller,
             a: dict.action || 'index'
         });
     }
};
exports.getActionInfo = function (url, method){
     var r = {controller: null , action: null , args: null },
         method = method ? method.toLowerCase() : 'get' ,
         // url: /blog/index?page=1 ,则pathname为: /blog/index
         pathname = parseURL(url).pathname;
     var m_routes = routes[method];
     for ( var i in m_routes){
         //正则匹配
         r.args = m_routes[i].u.exec(pathname);
         if (r.args){
             r.controller = m_routes[i].c;
             r.action = m_routes[i].a;
             r.args.shift(); //第一个值为匹配到的整个url,去掉
             break ;
         }
     }
     //如果匹配到route,r大概是 {controller:'blog', action:'index', args:['1']}
     return r;
};

map方法用于注册路由规则,我们新建一个config.js的文件,来配置route规则:

 

?
1
2
3
4
5
6
7
8
//config.js
var route = require( './route' );
route.map({
     method: 'get' ,
     url: /\/blog\/?$/i,
     controller: 'blog' ,
     action: 'index'
});

如果请求的url有匹配的route规则,则会返回controller和action信息。例如上面的route配置,当访问 /blog 这个url的时候,则会调用 ./controllers/blog.js 模块里面的index函数。

当调用action的时候,会传递controllerContext给acation: 

var ct = new controllerContext(req, res);
// 动态调用,动态语言就是方便啊
// 通过apply将controller的上下文对象传递给action
controller[actionInfo.action].apply(ct, actionInfo.args);

这里会通过apply将controllerContext作为action的this,并传递args作为action的参数来调用action。

ontrollerContext封装了一些action会用到的方法: 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
//controller的上下文对象
var controllerContext = function (req, res){
this .req = req;
this .res = res;
this .handler404 = handler404;
this .handler500 = handler500;
};
controllerContext.prototype.render = function (viewName, context){
viewEngine.render( this .req, this .res, viewName, context);
};
controllerContext.prototype.renderJson = function (json){
viewEngine.renderJson( this .req, this .res, json);
};

 在action中处理完逻辑获取获取到用户需要的数据后,就要呈现给用户。这就需要viewEngine来处理了。ViewEngine的代码如下:

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var viewEngine = {
     render: function (req, res, viewName, context){
         var filename = path.join(__dirname, 'views' , viewName);
         try {
             var output = Shotenjin.renderView(filename, context);
         } catch (err){
             handler500(req, res, err);
             return ;
         }
         res.writeHead(200, { 'Content-Type' : 'text/html' });
         res.end(output);
     },
     renderJson: function (res, json){
         //TODO:
     }
};

这里viewEngine主要负责模板解析。node有很多的可用的模块,模板解析模块也有一大堆,不过这里我们是要“玩”,所以模板解析系统我们这里使用jstenjin来稍作修改:

 

?
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
33
//shotenjin.js 增加的代码
//模板缓存,缓存解析后的模板
Shotenjin.templateCatch = {};
//读取模板内容
//在模板中引用模板使用: {# ../layout.html #}
Shotenjin.getTemplateStr = function (filename){
     //console.log('get template:' + filename);
     var t = '' ;
     //这里使用的是同步读取
     if (path.existsSync(filename)){
         t = fs.readFileSync(filename, 'utf-8' );
     } else {
         throw 'View: ' + filename + ' not exists' ;
     }
     t = t.replace(/\{ #[\s]*([\.\/\w\-]+)[\s]*#\}/ig, function(m, g1) {
         var fp = path.join(filename, g1.trim())
         return Shotenjin.getTemplateStr(fp);
     });
     return t;
};
Shotenjin.renderView = function (viewPath, context) {
     var template = Shotenjin.templateCatch[viewPath];
     if (!template){
         var template_str = Shotenjin.getTemplateStr(viewPath);
         var template = new Shotenjin.Template();
         template.convert(template_str);
         //添加到缓存中
         Shotenjin.templateCatch[viewPath] = template;
     }
     var output = template.render(context);
     return output;
};
global.Shotenjin = Shotenjin;

增加的代码主要是读取模板的内容,并解析模板中类似 {# ../layout.html #} 的标签,递归读取所有的模板内容,然后调用jstenjin的方法来解析模板。

这里读取文件内容使用的是fs.readFileSync,这是同步阻塞读取文件内容的,和我们平时使用的大多编程语言一样,而fs.readFile的非阻塞异步读。

这里的shotenjin.js原来是给客户端web浏览器javascript解析模板用的,现在拿到node.js来用,完全不用修改就正常工作。Google V8真威武。

现在基本的东西都完成了,但是对于静态文件,例如js、css等我们需要一个静态文件服务器:

 

 

?
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
var staticFileServer = function (req, res, filePath){
     if (!filePath){
         filePath = path.join(__dirname, config.staticFileDir, url.parse(req.url).pathname);
     }
     path.exists(filePath, function (exists) { 
         if (!exists) { 
             handler404(req, res); 
             return
        
         fs.readFile(filePath, "binary" , function (err, file) { 
             if (err) { 
                 handler500(req, res, err);
                 return
             }
             var ext = path.extname(filePath);
             ext = ext ? ext.slice(1) : 'html' ;
             res.writeHead(200, { 'Content-Type' : contentTypes[ext] || 'text/html' });
             res.write(file, "binary" );
             res.end();
         }); 
     });
};
var contentTypes = {
   "aiff" : "audio/x-aiff" ,
   "arj" : "application/x-arj-compressed"
   //省略
}

简单来说就是读取文件内容并写入到response中返回给客户端。

现在该有的都有了,我们写一个action: 

 

?
1
2
3
4
// ./controllers/blog.js
exports.index = function (){
     this .render( 'blog/index.html' , {msg: 'Hello World' });
};

blog/index.html的内容为: 

{# http://www.cnblogs.com/header.html #}
< h3 class ="title" > n2Mvc Demo </ h3 >
< h1 > #{msg} </ h1 >
{# http://www.cnblogs.com/footer.html #}

 接着,就是写一个脚本来启动我们的n2Mvc了:

// run.js
var n2MvcServer = require( ' ./server ' );
n2MvcServer.runServer();

ok,运行我们的启动脚本:

在浏览器访问看看:

clip_image006[7]

嗯嗯,一切正常。

 

好,接下来我们再写一个获取新浪微博最新微博的页面。首先,我们在config.js中增加一个route配置:

 

?
1
2
3
4
5
6
route.map({
     method: 'get' ,
     url: /\/tweets\/?$/i,
     controller: 'blog' ,
     action: 'tweets'
});

然后开始写我们的cnotroller action:

 

?
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
var http = require( 'http' ),
     events = require( "events" );
var tsina_client = http.createClient(80, "api.t.sina.com.cn" );
var tweets_emitter = new events.EventEmitter();
// action: tweets
exports.tweets = function (blogType){
     var _t = this ;
     var listener = tweets_emitter.once( "tweets" , function (tweets) { 
         _t.render( 'blog/tweets.html' , {tweets: tweets});
     });
     get_tweets();
};
function get_tweets() {
     var request = tsina_client.request( "GET" , "/statuses/public_timeline.json?source=3243248798" , { "host" : "api.t.sina.com.cn" });
     request.addListener( "response" , function (response) {
         var body = "" ;
         response.addListener( "data" , function (data) {
             body += data;
         });
         response.addListener( "end" , function () {
             var tweets = JSON.parse(body);
             if (tweets.length > 0) {
                 console.log( 'get tweets \n' );
                 tweets_emitter.emit( "tweets" , tweets);
             }
         });
     });
     request.end();
}

这里使用http.createClient来发送请求获取新浪微博的最新微博,然后注册相应事件的监听。这里详细说下node的事件系统:EventEmitter。 EventEmitter可以通过require('events'). EventEmitter来访问,创建一个 EventEmitter的实例emitter后,就可以通过这个emitter来注册、删除、发出事件了。

例如上面的代码中,先创建来一个EventEmitter的实例:

var tweets_emitter = new events.EventEmitter();

然后用once注册一个一次性的事件监听:

var listener = tweets_emitter.once( " tweets " , function (tweets) {
_t.render( ' blog/tweets_data.html ' , {tweets: tweets});
});

once注册的事件在事件被触发一次后,就会自动移除。

最后,通过emit来发出事件:

tweets_emitter.emit( " tweets " , tweets);

这样,整个事件的流程都清晰了。

下面写一下显示tweets的模板: 

tweets.html
复制代码
< ul >
<? js for(var i in tweets){ ?>
<? js var tweet = tweets[i], user = tweets[i].user; ?>
< li >
< div class ="usericon" >
< a class ="user_head" href ="###" >
< img src ="#{user.profile_image_url}" />
</ a >
</ div >
< div class ="mainContent" >
< div class ="userName" >
< a href ="###" >
#{user.screen_name}
</ a >
</ div >
< div class ="msg" >
< div class ="tweet" >
< div class ="tweet_text" >
#{tweet.text}
</ div >
<? js if(tweet.thumbnail_pic){ ?>
< div >
< a target ="_blank" href ="javascript:void(0);" class ="thumbnail_pic" >
< img class ="pic" src ="#{tweet.thumbnail_pic}" />
</ a >
</ div >
<? js } ?>
</ div >
</ div >
</ div >
< br class ="clear" />
</ li >
<? js } ?>
</ ul >
复制代码

 

万事大吉,运行并访问:

clip_image008[6]

 

附一个简单的和Django的对比测试

 

 

后记

本文写的mvc框架完全是为了尝试node.js,其实node已经有很多的 Modules可以用,也有比较成熟的web框架Express。如果你要实现一个模块之前,可以先到node的modules页面查找下有没有你需要的模块先。

本文示例源代码:learnNode.zip

 

 

一些资源:

 node.js的文档:http://nodejs.org/api.html

How To Node:  http://howtonode.org

Node.js:用JavaScript写服务器端程序-介绍并写个MVC框架

 

(注:1、本文基于Node.js V0.3.6; 2、本文假设你了解JavaScript; 3、本文假设你了解MVC框架;4、本文作者:QLeelulu,转载请注明出处。5、本文示例源代码:learnNode.zip)

 

Node.js是什么

Node让你可以用javascript编写服务器端程序,让javascript脱离web浏览器的限制,像C#、JAVA、Python等语言一样在服务器端运行,这也让一些熟悉Javascript的前端开发人员进军到服务器端开发提供了一个便利的途径。 Node是基于Google的V8引擎封装的,并提供了一些编写服务器程序的常用接口,例如文件流的处理。Node的目的是提供一种简单的途径来编写高性能的网络程序。

Node.js的性能

hello world 测试:

clip_image002

300并发请求,返回不同大小的内容:

clip_image004

为什么node有如此高的性能?看node的特性。

Node.js的特性

1. 单线程

2. 非阻塞IO

3. Google V8

4. 事件驱动

更详细的了解node请看淘宝UED博客上的关于node.js的一个幻灯片:http://www.slideshare.net/lijing00333/node-js

你好,世界

这,当然是俗套的Hello World啦(hello_world.js):

var http = require( ' http');
http.createServer( function (req, res) {
res.writeHead( 200 , { ' Content-Type': 'text/plain'});
res.end( ' Hello World\n');
}).listen( 8124 , " 127.0.0.1 " );
console.log( ' Server running at http://127.0.0.1:8124/');

require类似于C#的using、Python的import,用于导入模块(module)。node使用的是CommonJS的模块系统。http.createServer 的参数为一个函数,每当有新的请求进来的时候,就会触发这个函数。最后就是绑定要监听的端口。

怎么运行?

当然,是先安装node.js啦。到http://nodejs.org/下载并编译,支持Linux、Mac,也支持windows下的Cygwin。具体的安装说明见:http://howtonode.org/how-to-install-nodejs

装好node后,就可以运行我们的hello world了:

$ node hello_world.js
Server running at http: // 127.0 . 0.1 : 8124 /

 

clip_image006

编程习惯的改变?

我们来写一个读取文件内容的脚本:

 

?
1
2
3
4
5
6
7
8
9
10
11
//output_me.js
var fs = require( 'fs' ), fileContent = 'nothing' ;
fs.readFile(__filename, "utf-8" , function (err, file) { 
     if (err) { 
         console.log(err);
         return
     }
     fileContent = file;
     console.log( 'end readfile \n' );
});
console.log( 'doSomethingWithFile: ' + fileContent + '\n' );

这个脚本读取当前文件的内容并输出。__filename是node的一个全局变量,值为当前文件的绝对路径。我们执行这个脚本看一下:

clip_image008

有没发现结果不对呢?打印的fileContent并不是读取到的文件内容,而是初始化的时候赋值的nothing,并且‘end readfile’最后才打印出来。前面我们提到node的一个特性就是非阻塞IO,而readFile就是异步非阻塞读取文件内容的,所以后面的代码并不会等到文件内容读取完了再执行。请谨记node的异步非阻塞IO特性。所以我们需要将上面的代码修改为如下就能正常工作了:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
//output_me.js
var fs = require( 'fs' ), fileContent = 'nothing' ;
fs.readFile(__filename, "utf-8" , function (err, file) { 
     if (err) { 
         console.log(err);
         return
     }
     fileContent = file;
     //对于file的处理放到回调函数这里处理
     console.log( 'doSomethingWithFile: ' + fileContent + '\n' );
});
console.log( '我们先去喝杯茶\n' );

写个Web MVC框架试试

下面我们用node来写一个小玩具:一个Web MVC框架。这个小玩具我称它为n2Mvc,它的代码结构看起来大概如下:

clip_image010

和hello world一样,我们需要一个http的服务器来处理所有进来的请求:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var http = require( 'http' ),
     querystring = require( "querystring" );
exports.runServer = function (port){
     port = port || 8080;
     var server = http.createServer( function (req, res){
         var _postData = '' ;
         //on用于添加一个监听函数到一个特定的事件
         req.on( 'data' , function (chunk)
         {
             _postData += chunk;
         })
         .on( 'end' , function ()
         {
             req.post = querystring.parse(_postData);
             handlerRequest(req, res);
         });
     }).listen(port);
     console.log( 'Server running at http://127.0.0.1:' + port + '/' );
};

这里定义了一个runServer的方法来启动我们的n2Mvc的服务器。有没注意到runServer前面有个exports?这个exports相当于C#中的publish,在用require导入这个模块的时候,runServer可以被访问到。我们写一个脚本来演示下node的模块导入系统:

 

?
1
2
3
4
5
//moduleExample.js
var myPrivate = '艳照,藏着' ;
exports.myPublish = '冠西的相机' ;
this .myPublish2 = 'this也可以哦' ;
console.log( 'moduleExample.js loaded \n' );

执行结果:

clip_image002[9]

从结果中我们可以看出exports和this下的变量在外部导入模块后,可以被外部访问到,而var定义的变量只能在脚本内部访问。

从结果我们还可以看出,第二次require导入moduleExample模块的时候,并没有打印“moduleExample.js loaded”,因为require导入模块的时候,会先从require.cache 中检查模块是否已经加载,如果没有加载,才会从硬盘中查找模块脚本并加载。

require支持相对路径查找模块,例如上面代码中require('./moduleExample')中的“./”就代表在当前目录下查找。如果不是相当路径,例如 require('http'),node则会到require.paths中去查找,例如我的系统require.paths为:

clip_image004[7]

当require('http')的时候,node的查找路径为:

复制代码
1 、 / home / qleelulu / .node_modules / http
2 、 / home / qleelulu / .node_modules / http.js
3 、 / home / qleelulu / .node_modules / http.node
4 、 / home / qleelulu / .node_modules / http / index.js
5 、 / home / qleelulu / .node_modules / http / index.node
6 、 / home / qleelulu / .node_libraries / http
7 、 / home / qleelulu / .node_libraries / http.js
8 、参考前面
复制代码

再看回前面的代码,http.createServer中的回调函数中的request注册了两个事件,前面提到过node的一个特点是事件驱动的,所以这种事件绑定你会到处看到(想想jQuery的事件绑定?例如$('a').click(fn))。关于node的事件我们在后面再细说。request对象的data事件会在接收客户端post上来的数据时候触发,而end事件则会在最后触发。所以我们在data事件里面处理接收到的数据(例如post过来的form表单数据),在end事件里面通过handlerRequest 函数来统一处理所有的请求并分发给相应的controller action处理。

handlerRequest的代码如下:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var route = require( './route' );
var handlerRequest = function (req, res){
     //通过route来获取controller和action信息
     var actionInfo = route.getActionInfo(req.url, req.method);
     //如果route中有匹配的action,则分发给对应的action
     if (actionInfo.action){
         //假设controller都放到当前目录的controllers目录里面,还记得require是怎么搜索module的么?
         var controller = require( './controllers/' +actionInfo.controller); // ./controllers/blog
         if (controller[actionInfo.action]){
             var ct = new controllerContext(req, res);
             //动态调用,动态语言就是方便啊
             //通过apply将controller的上下文对象传递给action
             controller[actionInfo.action].apply(ct, actionInfo.args);
         } else {
             handler500(req, res, 'Error: controller "' + actionInfo.controller + '" without action "' + actionInfo.action + '"' )
         }
     } else {
         //如果route没有匹配到,则当作静态文件处理
         staticFileServer(req, res);
     }
};

这里导入来一个route模块,route根据请求的url等信息去获取获取controller和action的信息,如果获取到,则通过动态调用调用action方法,如果没有匹配的action信息,则作为静态文件处理。

下面是route模块的代码:

 

?
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
33
34
35
36
37
38
39
40
41
42
var parseURL = require( 'url' ).parse;
//根据http请求的method来分别保存route规则
var routes = {get:[], post:[], head:[], put:[], delete :[]};
/**
* 注册route规则
* 示例:
* route.map({
*     method:'post',
*     url: /\/blog\/post\/(\d+)\/?$/i,
*     controller: 'blog',
*     action: 'showBlogPost'
* })
*/
exports.map = function (dict){
     if (dict && dict.url && dict.controller){
         var method = dict.method ? dict.method.toLowerCase() : 'get' ;
         routes[method].push({
             u: dict.url, //url匹配正则
             c: dict.controller,
             a: dict.action || 'index'
         });
     }
};
exports.getActionInfo = function (url, method){
     var r = {controller: null , action: null , args: null },
         method = method ? method.toLowerCase() : 'get' ,
         // url: /blog/index?page=1 ,则pathname为: /blog/index
         pathname = parseURL(url).pathname;
     var m_routes = routes[method];
     for ( var i in m_routes){
         //正则匹配
         r.args = m_routes[i].u.exec(pathname);
         if (r.args){
             r.controller = m_routes[i].c;
             r.action = m_routes[i].a;
             r.args.shift(); //第一个值为匹配到的整个url,去掉
             break ;
         }
     }
     //如果匹配到route,r大概是 {controller:'blog', action:'index', args:['1']}
     return r;
};

map方法用于注册路由规则,我们新建一个config.js的文件,来配置route规则:

 

?
1
2
3
4
5
6
7
8
//config.js
var route = require( './route' );
route.map({
     method: 'get' ,
     url: /\/blog\/?$/i,
     controller: 'blog' ,
     action: 'index'
});

如果请求的url有匹配的route规则,则会返回controller和action信息。例如上面的route配置,当访问 /blog 这个url的时候,则会调用 ./controllers/blog.js 模块里面的index函数。

当调用action的时候,会传递controllerContext给acation: 

var ct = new controllerContext(req, res);
// 动态调用,动态语言就是方便啊
// 通过apply将controller的上下文对象传递给action
controller[actionInfo.action].apply(ct, actionInfo.args);

这里会通过apply将controllerContext作为action的this,并传递args作为action的参数来调用action。

ontrollerContext封装了一些action会用到的方法: 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
//controller的上下文对象
var controllerContext = function (req, res){
this .req = req;
this .res = res;
this .handler404 = handler404;
this .handler500 = handler500;
};
controllerContext.prototype.render = function (viewName, context){
viewEngine.render( this .req, this .res, viewName, context);
};
controllerContext.prototype.renderJson = function (json){
viewEngine.renderJson( this .req, this .res, json);
};

 在action中处理完逻辑获取获取到用户需要的数据后,就要呈现给用户。这就需要viewEngine来处理了。ViewEngine的代码如下:

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var viewEngine = {
     render: function (req, res, viewName, context){
         var filename = path.join(__dirname, 'views' , viewName);
         try {
             var output = Shotenjin.renderView(filename, context);
         } catch (err){
             handler500(req, res, err);
             return ;
         }
         res.writeHead(200, { 'Content-Type' : 'text/html' });
         res.end(output);
     },
     renderJson: function (res, json){
         //TODO:
     }
};

这里viewEngine主要负责模板解析。node有很多的可用的模块,模板解析模块也有一大堆,不过这里我们是要“玩”,所以模板解析系统我们这里使用jstenjin来稍作修改:

 

?
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
33
//shotenjin.js 增加的代码
//模板缓存,缓存解析后的模板
Shotenjin.templateCatch = {};
//读取模板内容
//在模板中引用模板使用: {# ../layout.html #}
Shotenjin.getTemplateStr = function (filename){
     //console.log('get template:' + filename);
     var t = '' ;
     //这里使用的是同步读取
     if (path.existsSync(filename)){
         t = fs.readFileSync(filename, 'utf-8' );
     } else {
         throw 'View: ' + filename + ' not exists' ;
     }
     t = t.replace(/\{ #[\s]*([\.\/\w\-]+)[\s]*#\}/ig, function(m, g1) {
         var fp = path.join(filename, g1.trim())
         return Shotenjin.getTemplateStr(fp);
     });
     return t;
};
Shotenjin.renderView = function (viewPath, context) {
     var template = Shotenjin.templateCatch[viewPath];
     if (!template){
         var template_str = Shotenjin.getTemplateStr(viewPath);
         var template = new Shotenjin.Template();
         template.convert(template_str);
         //添加到缓存中
         Shotenjin.templateCatch[viewPath] = template;
     }
     var output = template.render(context);
     return output;
};
global.Shotenjin = Shotenjin;

增加的代码主要是读取模板的内容,并解析模板中类似 {# ../layout.html #} 的标签,递归读取所有的模板内容,然后调用jstenjin的方法来解析模板。

这里读取文件内容使用的是fs.readFileSync,这是同步阻塞读取文件内容的,和我们平时使用的大多编程语言一样,而fs.readFile的非阻塞异步读。

这里的shotenjin.js原来是给客户端web浏览器javascript解析模板用的,现在拿到node.js来用,完全不用修改就正常工作。Google V8真威武。

现在基本的东西都完成了,但是对于静态文件,例如js、css等我们需要一个静态文件服务器:

 

 

?
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
var staticFileServer = function (req, res, filePath){
     if (!filePath){
         filePath = path.join(__dirname, config.staticFileDir, url.parse(req.url).pathname);
     }
     path.exists(filePath, function (exists) { 
         if (!exists) { 
             handler404(req, res); 
             return
        
         fs.readFile(filePath, "binary" , function (err, file) { 
             if (err) { 
                 handler500(req, res, err);
                 return
             }
             var ext = path.extname(filePath);
             ext = ext ? ext.slice(1) : 'html' ;
             res.writeHead(200, { 'Content-Type' : contentTypes[ext] || 'text/html' });
             res.write(file, "binary" );
             res.end();
         }); 
     });
};
var contentTypes = {
   "aiff" : "audio/x-aiff" ,
   "arj" : "application/x-arj-compressed"
   //省略
}

简单来说就是读取文件内容并写入到response中返回给客户端。

现在该有的都有了,我们写一个action: 

 

?
1
2
3
4
// ./controllers/blog.js
exports.index = function (){
     this .render( 'blog/index.html' , {msg: 'Hello World' });
};

blog/index.html的内容为: 

{# http://www.cnblogs.com/header.html #}
< h3 class ="title" > n2Mvc Demo </ h3 >
< h1 > #{msg} </ h1 >
{# http://www.cnblogs.com/footer.html #}

 接着,就是写一个脚本来启动我们的n2Mvc了:

// run.js
var n2MvcServer = require( ' ./server ' );
n2MvcServer.runServer();

ok,运行我们的启动脚本:

在浏览器访问看看:

clip_image006[7]

嗯嗯,一切正常。

 

好,接下来我们再写一个获取新浪微博最新微博的页面。首先,我们在config.js中增加一个route配置:

 

?
1
2
3
4
5
6
route.map({
     method: 'get' ,
     url: /\/tweets\/?$/i,
     controller: 'blog' ,
     action: 'tweets'
});

然后开始写我们的cnotroller action:

 

?
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
var http = require( 'http' ),
     events = require( "events" );
var tsina_client = http.createClient(80, "api.t.sina.com.cn" );
var tweets_emitter = new events.EventEmitter();
// action: tweets
exports.tweets = function (blogType){
     var _t = this ;
     var listener = tweets_emitter.once( "tweets" , function (tweets) { 
         _t.render( 'blog/tweets.html' , {tweets: tweets});
     });
     get_tweets();
};
function get_tweets() {
     var request = tsina_client.request( "GET" , "/statuses/public_timeline.json?source=3243248798" , { "host" : "api.t.sina.com.cn" });
     request.addListener( "response" , function (response) {
         var body = "" ;
         response.addListener( "data" , function (data) {
             body += data;
         });
         response.addListener( "end" , function () {
             var tweets = JSON.parse(body);
             if (tweets.length > 0) {
                 console.log( 'get tweets \n' );
                 tweets_emitter.emit( "tweets" , tweets);
             }
         });
     });
     request.end();
}

这里使用http.createClient来发送请求获取新浪微博的最新微博,然后注册相应事件的监听。这里详细说下node的事件系统:EventEmitter。 EventEmitter可以通过require('events'). EventEmitter来访问,创建一个 EventEmitter的实例emitter后,就可以通过这个emitter来注册、删除、发出事件了。

例如上面的代码中,先创建来一个EventEmitter的实例:

var tweets_emitter = new events.EventEmitter();

然后用once注册一个一次性的事件监听:

var listener = tweets_emitter.once( " tweets " , function (tweets) {
_t.render( ' blog/tweets_data.html ' , {tweets: tweets});
});

once注册的事件在事件被触发一次后,就会自动移除。

最后,通过emit来发出事件:

tweets_emitter.emit( " tweets " , tweets);

这样,整个事件的流程都清晰了。

下面写一下显示tweets的模板: 

tweets.html
复制代码
< ul >
<? js for(var i in tweets){ ?>
<? js var tweet = tweets[i], user = tweets[i].user; ?>
< li >
< div class ="usericon" >
< a class ="user_head" href ="###" >
< img src ="#{user.profile_image_url}" />
</ a >
</ div >
< div class ="mainContent" >
< div class ="userName" >
< a href ="###" >
#{user.screen_name}
</ a >
</ div >
< div class ="msg" >
< div class ="tweet" >
< div class ="tweet_text" >
#{tweet.text}
</ div >
<? js if(tweet.thumbnail_pic){ ?>
< div >
< a target ="_blank" href ="javascript:void(0);" class ="thumbnail_pic" >
< img class ="pic" src ="#{tweet.thumbnail_pic}" />
</ a >
</ div >
<? js } ?>
</ div >
</ div >
</ div >
< br class ="clear" />
</ li >
<? js } ?>
</ ul >
复制代码

 

万事大吉,运行并访问:

clip_image008[6]

 

附一个简单的和Django的对比测试

 

 

后记

本文写的mvc框架完全是为了尝试node.js,其实node已经有很多的 Modules可以用,也有比较成熟的web框架Express。如果你要实现一个模块之前,可以先到node的modules页面查找下有没有你需要的模块先。

本文示例源代码:learnNode.zip

 

 

一些资源:

 node.js的文档:http://nodejs.org/api.html

How To Node:  http://howtonode.org

Node.js中文社区: http://cnodejs.org (由淘宝的工程师发起)

learning-serverside-javascript-with-node-js: http://net.tutsplus.com/tutorials/javascript-ajax/learning-serverside-javascript-with-node-js/ (中文翻译版:http://www.osseye.com/?p=456 )

 

 

 

Node.js中文社区: http://cnodejs.org (由淘宝的工程师发起)

learning-serverside-javascript-with-node-js: http://net.tutsplus.com/tutorials/javascript-ajax/learning-serverside-javascript-with-node-js/ (中文翻译版:http://www.osseye.com/?p=456 )

 

 

posted on 2012-05-07 09:58 放逐忧伤 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/navy235/archive/2012/05/07/2486963.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值