Node.js实现一个简单的Web MVC框架 - 我要看明白的第一个mvc 下载本文

而end事件则会在最后触发。所以我们在data事件里面处理接收到的数据(例如post过来的form表单数据),在end事件里面通过handlerRequest 函数来统一处理所有的请求并分发给相应的controller action处理。 handlerRequest的代码如下:

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.action + '\如果route没有匹配到,则当作静态文件处理 staticFileServer(req, res); } }; 这里导入来一个route模块,route根据请求的url等信息去获取获取controller和action的信息,如果获取到,则通过动态调用调用action方法,如果没有匹配的action信息,则作为静态文件处理。 下面是route模块的代码:

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规则:

//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会用到的方法:

//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的代码如下:

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来稍作修改:

//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等我们需要一个静态文件服务器:

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, \return; } var ext = path.extname(filePath); ext = ext ? ext.slice(1) : 'html'; res.writeHead(200, {'Content-Type': contentTypes[ext] || 'text/html'}); res.write(file, \\省略 }

简单来说就是读取文件内容并写入到response中返回给客户端。 现在该有的都有了,我们写一个action:

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

blog/index.html的内容为: {# ../../header.html #}

n2Mvc Demo

#{msg}

{# ../../footer.html #}

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

// run.js var n2MvcServer = require('./server'); n2MvcServer.runServer(); ok,运行我们的启动脚本: 在浏览器访问看看:

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

route.map({ method:'get', url: /\\/tweets\\/?$/i, controller: 'blog', action: 'tweets' }); 然后开始写我们的cnotroller action:

var http = require('http'), events = require(\\function(blogType){ var _t = this; var listener = tweets_emitter.once(\{ _t.render('blog/tweets.html', {tweets: tweets}); }); get_tweets(); }; function get_tweets() { var request = tsina_client.request(\request.addListener(\{ body += data; }); response.addListener(\{ console.log('get tweets \\n'); tweets_emitter.emit(\

这里使用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}); });

once注册的事件在事件被触发一次后,就会自动移除。 最后,通过emit来发出事件: tweets_emitter.emit(\

这样,整个事件的流程都清晰了。 下面写一下显示tweets的模板: 万事大吉,运行并访问:

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