本文提供了一个模仿ASP.Net MVC的node.js MVC框架,基本实现了ASP.Net MVC的Model(模型)、View(视图)和Controller(控制器)的这三项基本功能,并且基于这个MVC框架的编码方式、程序结构和文件组织,同ASP.Net MVC基本相同。
一:为什么要做这个东西
首先声明,我对ASP.Net MVC研究不深,对node.js更是仅知皮毛。
多年来我主要做ASP.Net WebForm的开发,近来偶然接触了MVC,令人耳目一新。感觉MVC同WebForm有很大的区别,源代码更简洁、目标代码更干净、系统运行速度更快。虽然还没有找到类似于WebForm的可视化开发的方法,不过对我这种已经习惯手写代码的程序员而言,却没什么障碍。
近来又接触了node.js,这真是一个神奇的东西:JavaScript做服务器端开发、代码及其简洁、(据说)速度也奇快。不过......不过对我这种已经习惯了ASP.Net开发方式的程序员,node.js仅提供基础功能支持,这对我们却是一种折磨。
灵机一动,或者说,是为了以后的懒惰做准备,决定写一个简单的MVC框架。基本原则是:其使用方法同ASP.Net MVC基本相同(为了让我们少耗费一些脑细胞)。
这个MVC框架我已经基本写完(草稿),本文就仅对已有的实现做简要说明,中间的挫折统统忽略吧。
另外,这个MVC框架用到了一些第三方组件,这里提供其官方地址,若需要下载或对其有疑问,直接导航过去看:
- underscore:一个实用的JavaScript工具库,提供很多对集合、数组、对象的快捷操作方法,地址:http://documentcloud.github.com/underscore/
- underscore.string:另一个实用的JavaScript工具库,提供对字符串的快捷操作方法(实际上同underscore没有任何关系),地址:https://github.com/epeli/underscore.string
- ejs:一个强大的JavaScript模板支持库,地址:https://github.com/visionmedia/ejs
先具体说一下导致我产生这个想法的具体原因。
为了学习node.js,从官方网站(http://www.nodejs.org/)下载程序包,执行安装。(安装完成后,可执行文件node.exe放在C:\Program Files\nodejs中,如果不习惯的话,可以把整个文件夹拷贝到其他的位置。)
首先,从Hello Word开始。在nodejs文件夹中新建一个文本文件,起名为index.js,用任意文本编辑器编辑:
1 var http = require('http');
2 http.createServer(function (req, res) {
3 res.end('Hello World\n');
4 }).listen(81, "127.0.0.1");
5 console.log('Server running at http://127.0.0.1:81/');
然后在Dos窗口执行: node index.js。用浏览器打开:http://127.0.0.1:81,马上就能看到文本:Hello World。
是不是很简单?不过怨念也就在这里了:我们做传统的Web开发,每个请求需要建立不同的文件,并使用不同的http地址来请求;而node.js,所有的请求都指向到这个根文件夹的index.js,这就需要我们经常性的在这个文件中添加代码,即使是使用伟大的express也不例外。这是express的一个样例,一般起名为app.js:
1 var express = require('express ');
2
3 var app = express.createServer();
4
5 app.get('/', function(req, res){
6 res.send('root index');
7 });
8
9 app.get('/user/', function(req, res){
10 res.send('user's index');
11 });
12
13 app.listen(82);
14 console.log('listening on port 82');
当然,我不会去说express不好(因为这确实是个强大的组件),只是我不习惯;可能express有很好的方法,我不知道而已。
于是我决定用node.js做一个类似于ASP.Net MVC的框架,以方便我以后继续学习使用node.js,或许也可以帮助到其他初学node.js的同学们。
二:这个东西应该是什么样子的
既然是计划模仿ASP.Net MVC,那么就要尽量模仿的像一点。
为什么我要模仿ASP.Net MVC,而不去模仿ASP.Net WebForm: 因为MVC够简单,而WebForm太复杂了,尤其是那一大堆服务器端控件,除非一个传奇级的任务,可不敢想象怎么去做。
首先是ASP.Net MVC的目录结构。这个图片就是一个经典的ASP.Net MVC站点的目录结构(VS自动创建的,做ASP.Net MVC的同学应该非常熟悉) :
在这个图片中,有几个东西是我们需要关注的:
- Content文件夹:用于放置静态文件,包括images、styles等等
- Controllers文件夹:用于放置控制器程序文件
- Models文件夹:用于放置数据模型文件
- Views文件夹:用于放置视图文件(就是页面或控件模板)
上述的四个文件夹是我们需要模仿的,而App_Start、Global.asax、Web.config等等的,或许到了后期会发现也需要做,现在就先算了。至于Scripts文件夹的内容,在我看来应该放到Content\Scripts子文件夹中 。
作为适应这个框架工作方式的结果,或者说是我们的node.js MVC的目的:
- 全部静态文件放入Content文件夹,并可以通过HTTP地址直接访问,而不用增加任何程序代码
- 全部控制器文件放入Controllers文件夹,一个Controller文件包含有多个Action,每个Action的访问方式为:http://.../<controller>/<action>/...
- 全部视图文件放入Views文件夹,并且每个Controller在这里对应有一个同名的子文件夹
- ASP.Net MVC的全部视图文件放入Models文件夹,不过node.js是基于javascript动态语言的,模型定义似乎并不重要,这里就忽略了
为实现上述目的,我们就先建立一个基本的目录结构,之后的工作都围绕这个目录结构:
在这个框架中:
- index.js文件永远不用修改
- contents文件夹一般会包含images、script、styles等子文件夹,任何文件放进去就可以用,并且可以包含任意规格的子文件夹
- controllers文件夹包含全部的服务器端javascript文件
- views文件夹包含全部的服务器端模板文件,且每一个controler在此文件夹中对应一个同名的子文件夹
三:这个框架都实现了什么
这个框架的程序基本程序逻辑如下图所示:
根据以上逻辑图可以看出,此框架只有三个简单的分支:
- 静态请求直接输出
- 动态请求由控制器处理
- 若需要输出页面则调用模板引擎处理
这里先说明一下:在此框架中,任何有文件后缀的请求均被认为是静态请求。
为实现此框架编写了一系列的程序文件,以实现以下功能:
- 请求入口:即index.js文件,实现请求分发和简单的安全管理
- 静态页处理:读取contents文件夹中的静态文件
- 动态请求处理:处理动态请求并返回动态结果
- Session支持:提供Web应用中的用户级缓存支持(等同于ASP.Net中的Session)
- Cache支持:提供Web应用中的全局缓存支持(等同于ASP.Net中的Cache或Application)
- 模板处理:处理包含Html页面或板块的文件,并支持嵌入式开发
- 嵌入式代码支持:为嵌入式开发提供扩展支持,包括ASP.Net MVC中的Html语法
- 路由:用于支持除了“/<controller>/<action>/...”之外的其他浏览器请求(暂未实现)
全部文件夹和文件结构如下表所示:
index.js:请求入口 - contents:静态内容文件夹 - css - images - scripts - controllers:动态内容程序文件夹 - views:动态内容模板文件夹 - node_modules:node.js类库文件夹 - ejs:第三方模板处理组件 - mvc:我们的MVC框架文件夹 base.js:进行动态请求初始化操作,例如准备session等 cache.js:支持全局缓存 html.js:支持模板处理和嵌入式开发 index.js:无功能,仅用于nodejs mvc.js:请求分发和处理 routes.js:请求路由 session.js:支持用户缓存 utils.js:部分常用工具函数 - underscore:第三方类库 - underscore.string:第三方类库
四:MVC框架实现代码
注意:我假定阅读本文的同学:
- 对javascript比较熟悉(不能是生手,否则可能看不懂这些代码)
- 对node.js已经有了初步的了解,比如require、module.exports等的用法
1. 请求入口(index.js)
作为一个标准的node.js程序,一个根文件夹下的index.js是必须的。这个文件仅包含如下代码:
1 var http = require('http'); 2 var mvc = require('mvc'); 3 4 http.createServer(function (req, res) { 5 new mvc.startRequest(req, res); 6 }).listen(88, "127.0.0.1"); 7 8 console.log('Server running at http://127.0.0.1:88/');
在这个文件中,第6行的listen方法的参数(及第8行的日志)需要根据实际情况进行修改,其他的可以总是保持不动。当然若实现了类似ASP.Net的web.config支持之后,这个index.js文件就再也不用修改了。
在index.js文件中仅做了一件事,即调用mvc.startRequest方法以开始一个请求处理。其他的代码同node.js中的hello world样例程序没啥区别。
2. 分发与请求处理 (mvc.js)
mvc.js程序是此框架的核心,主要实现了请求分发和处理入口。
首先,采用node.js推荐的方法引入外部程序:
1 var url = require('url'); 2 var path = require('path'); 3 var fs = require('fs'); 4 var querystring = require('querystring'); 5 var _ = require('underscore'); 6 var _s = require('underscore.string'); 7 var utils = require('./utils'); 8 var routes = require('./routes'); 9 var base = require('./base');
其中,1-4是node.js系统类库,5-6是第三方类库,7-9是我们的框架中的将被用到的类库(后面我们在详细介绍)。
然后我们看看在index.js用到的mvc.startRequest方法,在这个方法中我们简单地根据有无文件后缀来区分是否是动态请求。
1 module.exports.startRequest = function(req, res) { 2 //在处理请求前先在控制台打印日志 3 if (req.url!='/favicon.ico') { //favicon.ico文件一般情况下是浏览器自动请求的,对我们没用 4 console.log(_s.sprintf('Start request: %s\t%s', utils.dateString(new Date()), req.url)); 5 } 6 //初始化一个请求处理引擎实例 7 var handler = new RequestHandler(req, res) 8 var u = url.parse(req.url); 9 var ext = path.extname(u.pathname); 10 if (ext) { //如果请求的文件有后缀则认为是静态请求 11 handler.responseContent(u.pathname); 12 } 13 else { //否则是动态请求 14 handler.handleDynamicRequest(); 15 } 16 }
注:为方便起见,代码说明我尽量都卸载代码注释中。
上述代码中我们用到了一个RequestHandler类,用于处理请求。以下是这个类的定义:
1 function RequestHandler(req, res) { 2 this.request = req; 3 this.response = res; 4 }
我们使用RequestHander.responseContent方法来处理任何静态请求,即存储在/contents/文件夹中的文件。思路是:找到文件、读取文件、输出文件。
1 RequestHandler.prototype.responseContent = function(filename) { 2 if (!filename) { //如果参数缺失,则直接提示错误(不过这种情况应该不会出现) 3 this.responseNoFound(); 4 } 5 else { 6 filename = _s.trim(filename,' \/\\'); 7 8 var self = this; 9 var filepath = path.resolve('./contents/', filename); 10 fs.readFile(filepath, function (err, data) { 11 if (err) { //如果文件不存在或无法访问,则提示错误 12 self.responseNoFound(); 13 } 14 else { //否则输出文件内容到客户端 15 self.response.write(data); 16 } 17 self.response.end(); //输出完成之后必须调用response.end方法 18 }); 19 } 20 }
我们使用RequestHandler.handleDynamicRequest方法来处理动态请求。思路是:获取请求参数,调用控制器来处理请求。
1 RequestHandler.prototype.handleDynamicRequest=function() { 2 var params = null; 3 var self = this; 4 if (self.request.method == 'POST') { //如果是post方式的请求,则需要异步读取请求参数 5 var buf = []; 6 self.request.addListener('data', function(chunk){ 7 buf.push(chunk); 8 }) 9 .addListener('end', function(){ 10 params = querystring.parse(buf.join('')); 11 self.handleController(params); 12 }) 13 } 14 else { //如果不是post方式(是get方式)的请求,则直接使用url参数 15 params = self.request.url.query; 16 if (_.isString(params)) 17 params = querystring.parse(params); 18 19 self.handleController(params); 20 } 21 }
上述代码中的RequestHandler.handleController方法,用于装载控制器,并执行action:
1 RequestHandler.prototype.handleController=function(params) { 2 var controllerInfo = routes.parseControllerInfo(this.request); //分析请求url,拆分出controller,action,parameters 3 4 if (controllerInfo 5 && controllerInfo.controllerName && controllerInfo.methodName 6 && !_s.startsWith(controllerInfo.controllerName, '_') 7 && !_s.startsWith(controllerInfo.methodName, '_')) { 8 9 var instance = this.createControllerInstance(controllerInfo.controllerName); //装载控制器 10 11 if (instance 12 && _.has(instance, controllerInfo.methodName) 13 && _.isFunction(instance[controllerInfo.methodName])) { //若存在action定义,则执行该action 14 try { //注意:这里必须加try...cache,以防止action发生错误导致我们的网站崩溃 15 var res = instance[controllerInfo.methodName](controllerInfo.parameters, params); 16 this.response.end(res + ""); 17 } 18 catch (err) { 19 console.log(_s.sprintf("Execute a controller method failed: %s", err)); 20 utils.responseError(this.response, 500); 21 } 22 } 23 else { 24 this.responseNoFound(); 25 } 26 } 27 else { 28 console.log(_s.sprintf('parse controller failed: %s', _.keys(controllerInfo))); 29 console.log(_s.sprintf('%s', _.values(controllerInfo))); 30 this.responseNoFound(); 31 } 32 }
装载控制器的RequestHandler.createControllerInstance方法的定义是:
1 RequestHandler.prototype.createControllerInstance = function(controllerName) { 2 var instance = null; 3 4 try { 5 var controller = require(path.resolve('./controllers/', controllerName)); // 使用require方法装载控制器 6 7 if (controller && _.isFunction(controller)) { 8 instance = new controller(this.request, this.response); 9 } 10 } 11 catch (err) { 12 utils.responseError(this.response, 500); 13 } 14 15 return instance; 16 }
在RequestHandler中还定义了两个额外的方法:
1 //初始化页面基类 2 module.exports.controller = function(req, res) { 3 return new base(req, res); 4 } 5 6 7 //当文件不存在时记录日志并提示错误 8 RequestHandler.prototype.responseNoFound = function() { 9 if (this.request.url!='/favicon.ico') { 10 console.log(_s.sprintf('No found url: %s', this.request.url)); 11 } 12 utils.responseError(this.response, 404); 13 }
3. 动态请求初始化(base.js)
在请求分发处理成功之后,我们就需要开始实际的业务处理,包括session、页面、模板等等。这些功能我们集中定义在base.js中:
1 var Cache = require('./cache'); 2 var Session = require('./session'); 3 var Html = require('./html'); 4 var routes = require('./routes'); 5 6 var BaseController = module.exports = function (req, res) { 7 this.request = req; 8 this.response = res; 9 10 this.cache = Cache; //全局缓存 11 this.session = Session.current(req, res); //用户缓存 12 this.html = new Html(this); //视图处理及嵌入式开发引擎 13 14 this.controllerInfo = routes.parseControllerInfo(req); 15 } 16 17 //处理一个视图(模板),并返回结果内容 18 BaseController.prototype.renderView = function(options) { 19 return this.html.renderView(options); 20 } 21 22 //处理一个视图(模板),直接输出到客户端 23 BaseController.prototype.responseView = function(options) { 24 this.response.end(this.renderView(options)); 25 }
4. 用户缓存处理 (session.js)
Session是Web应用程序中一个非常重要的东西。对node.js来说,Session处理其实就是一个全局的数据集合的处理:
1 var _ = require('underscore'); 2 var _s = require('underscore.string'); 3 4 //全部Session的存储空间 5 var SessionPool = {}; 6 7 //全局的Session过期时间 8 Session.prototype.expireTime = 20; //缺省20分钟 9 10 //新建一个SessionID 11 function newSessionID() { 12 var buff = []; 13 var chars = '0123456789abcdefghijklmnopqrstuvwxyz'; 14 for (var i=0; i<32; i++) { 15 buff.push(chars[Math.floor(Math.random() * 36)]); 16 } 17 return buff.join(''); 18 } 19 20 //一个Session的定义 21 var Session = function() { 22 var self = this; 23 self.sessionId = newSessionID(); 24 self.sessionItems = {}; 25 self._expireTime = Session.prototype.expireTime; 26 self._expireHandle = setInterval(function () { //每一分钟检查一次是否过期 27 self._expireTime -= 1; 28 if (self._expireTime <= 0) { 29 clearInterval(self._expireHandle); 30 self.dispose(); //过期则自动清除 31 } 32 }, 60000); 33 } 34 35 //清理一个Session 36 Session.prototype.dispose = function() { 37 if (this._expireHandle) { 38 clearInterval(this._expireHandle); 39 this._expireHandle = null; 40 } 41 42 for (var key in this.sessionItems) { 43 delete this.sessionItems[key]; 44 } 45 this.sessionItems = null; 46 47 delete SessionPool[this.sessionId]; 48 }
然后我们定义一些常用的Session操作方法:
1 //判断Session项是否存在 2 Session.prototype.has = function(key) { 3 return _.has(this.sessionItems, key); 4 }; 5 6 //添加或更新一个Session项 7 Session.prototype.set = function(key, value) { 8 if (key) { 9 this.refresh(); 10 11 if (this.has(key) && this.sessionItems[key] == value) { 12 return; 13 } 14 15 this.sessionItems[key] = value; 16 } 17 } 18 19 //获取一个Session项 20 Session.prototype.get = function(key) { 21 this.refresh(); 22 23 if (key && this.has(key)) { 24 return this.sessionItems[key]; 25 } 26 27 return null; 28 } 29 30 //删除一个Session项 31 Session.prototype.remove = function(key) { 32 this.refresh(); 33 34 if (key && this.has(key)) { 35 delete this.sessionItems[key]; 36 } 37 } 38 39 //刷新过期时间, 40 Session.prototype.refresh = function() { 41 this._expireTime = Session.prototype.expireTime; 42 } 43 44 module.exports = Session;
用户Session应该能被自动创建和恢复,这里我们使用大部分浏览器都支持的cookie(如果你的浏览器不支持cookie那么就需要另想它法了)。
1 module.exports.current = function(req, res) { 2 //从Request的header中存储的cookie中查找当前正在使用的SessionID 3 var headers = req.headers; 4 var sessionId = null, session = null; 5 var cookie = null; 6 if (_.has(headers, 'cookie')) { 7 cookie = headers['cookie']; 8 if (_s.startsWith(cookie, 'SessionID=')) { //此处可能不是很严谨,需要优化 9 sessionId = cookie.substr(10, 36); 10 } 11 } 12 13 //激活Session池中的一个session 14 if (sessionId && _.has(SessionPool, sessionId)) { 15 session = SessionPool[sessionId]; 16 } 17 18 //如果是新session,则自动创建一个,并写回cookie中 19 if (!session) { 20 session = new Session(); 21 SessionPool[session.sessionId] = session; 22 23 res.setHeader('set-cookie', 'SessionID=' + session.sessionId); 24 } 25 26 return session; 27 }
这个Session.current方法将在BaseController.js第11行被自动调用,然后在任何动态请求中就都可以所以用session了。
5. 系统缓存处理 (cache.js)
Cache同样是Web应用开发中的一个很重要的东西。它对应于ASP.Net中的Application集合,以及System.Web.Caching.Cache集合(ASP.Net中这两个东西可都可以实现全局缓存)。对node.js来说,Cache处理同Session差不多同样就是一个全局的数据集合的处理,不过它比Session要简单得多。
1 var _ = require('underscore'); 2 3 var CachePool = {}; 4 5 var Cache = function() {}; 6 7 //判断Cache是否存在 8 Cache.prototype.has = function(key) 9 { 10 return _.has(CachePool, key); 11 }; 12 13 //添加或更新一个Cache项 14 Cache.prototype.set = function(key, value) 15 { 16 if (key) { 17 CachePool[key] = value; 18 } 19 } 20 21 //获取一个Cache项 22 Cache.prototype.get = function(key) 23 { 24 if (key && Cache.prototype.has(key)) { 25 return CachePool[key]; 26 } 27 28 return null; 29 } 30 31 //移除一个Cache项 32 Cache.prototype.remove = function(key) { 33 if (Cache.prototype.has(key)) { 34 delete CachePool[key]; 35 } 36 } 37 38 //建立一个全局的Cache容器 39 module.exports = new Cache();
注意:上述代码的第39行直接建立了一个类实例,即保证整个应用中只有一个Cache实例,以保证全局的唯一性。
这个Cache将在BaseController.js第10行被自动调用,然后在任何动态请求中就都可以所以用cache了。
6. 视图处理 (html.js)
MVC构架中一个重要的功能点就是视图,简单地说就是模板处理。这里我们定义一个Html类(对应于ASP.Net MVC中的Html类)来实现这个功能。
基本思路是:装载模板文件、调用EJS处理模板。
1 var fs = require('fs'); 2 var path = require('path'); 3 var _ = require('underscore'); 4 var _s = require('underscore.string'); 5 var ejs = require('ejs'); 6 7 var Html = module.exports = function (controller) { 8 this.controller = controller; 9 }; 10 11 //处理一个模板返回结果内容 12 Html.prototype.renderView = function(options) { 13 //确定模板文件,若未指定,则缺省与控制器相同 14 var options = options || {}; 15 if (!options.path || !options.name) { 16 options.path = options.path || this.controller.controllerInfo.controllerName; 17 options.name = options.name || this.controller.controllerInfo.methodName; 18 } 19 options.path = _s.trim(options.path,' \/\\'); 20 //模板文件必须存在于views文件夹中 21 var viewPath = path.resolve(path.resolve('./views/', options.path + '/'), options.name); 22 if (options.name.indexOf('.')<0) { 23 options.extname = options.extname || '.htm'; 24 if (!_s.startsWith(options.extname,'.')) viewPath += '.'; 25 viewPath += options.extname; 26 } 27 28 try { //这里必须加try...cache,以防止文件操作及视图处理错误 29 var tmpl = this.controller.cache.get(viewPath); //首先从缓存中读取模板,以提高效率 30 if (!tmpl) { 31 tmpl = fs.readFileSync(viewPath); 32 33 if (!_.isString(tmpl)) { 34 tmpl = tmpl.toString(); 35 } 36 37 this.controller.cache.set(viewPath, tmpl) 38 } 39 40 //使用EJS引擎处理模板,并将一些可能需要的参数传入引擎 41 var res = ejs.render(tmpl, { 42 filename: viewPath, 43 model: options.data, 44 html: this.controller.html, 45 session: this.controller.session 46 }); 47 48 return res; 49 } 50 catch (err) { 51 console.log(_s.sprintf('Html.renderView error: %s', err)); 52 53 return null; 54 } 55 }
Q: 为什么我要使用EJS来处理模板,而不使用更稳定更流行underscore或jade等引擎?
A: 在选择模板处理引擎时,我重点考虑到了嵌入式开发的需求,即我应该可以将任何参数任何数据传入到模板引擎中,并参与模板的处理;而这个需求ejs做得更好。
6. 嵌入式开发 (html.js)
嵌入式开发,即在网页文件中,嵌入并运行程序代码。例如,以下的来自于ASP.Net MVC自动生成的一份代码:
<% if (Request.IsAuthenticated) { %> 欢迎使用 <b><%: Page.User.Identity.Name %></b>! [ <%: Html.ActionLink("注销", "LogOff", "Account") %> ] <% } else { %> [ <%: Html.ActionLink("登录", "LogOn", "Account") %> ] <% } %>
这其中包含三个关键点:
- 在页面中可以签入程序脚本,例如if...else...
- 页面中可以引用外部程序传入的数据对象,例如request, page, model,以及自定义对象等
- 支持Html.method的方式来创建页面控件
而这些,使用ejs都可以实现。
首先看上节代码中的第42-45行,我们在这里传入任何所需要的数据对象。同时,我们传入了一个Html对象,用于支持创建页面控件。
Html对象除了上节代码中定义的一个基本方法renderView之外,还需要定义更多的方法(具体需要哪些方法,请参见MSDN的在线帮助System.Web.Mvc.HtmlHelper对象)。
首先,我们需要定义一个生成控件的基本方法:
1 Html.prototype.element = function(tag, attributes) { 2 var buf = [], text=null; 3 4 buf.push('<'); 5 buf.push(tag); 6 if (attributes) { 7 for (var key in attributes) { 8 if (_.has(attributes, key)) { 9 var value = attributes[key]; 10 if (key.toLowerCase() === 'text') 11 text = value; 12 else if (value) 13 buf.push(" " + key + '="' + _.escape(value) + '"'); 14 } 15 } 16 } 17 if (!_.isNull(text)) { 18 buf.push('>'); 19 buf.push(text); 20 buf.push('</'+tag+'>'); 21 } 22 else { 23 buf.push('/>'); 24 } 25 26 return buf.join(''); 27 }
这个方法没什么好说的,根据数据构造一个字符串而已。
后面我们要做的,就是一个个编写控件生成代码。(下面是一些常用控件的生成代码)

1 Html.prototype.textBox = function(attributes) { 2 var multi = attributes.multi || attributes.multiline || attributes.multiLine; 3 var others = {}; 4 if (!multi) { 5 attributes.value = attributes.value || attributes.text; 6 delete attributes.text; 7 if (attributes.password) { 8 attributes.type = 'password'; 9 delete attributes.password; 10 return this.element('input', attributes); 11 } 12 else { 13 attributes.type = 'text'; 14 return this.element('input', attributes); 15 } 16 } 17 else { 18 attributes.text = attributes.text || attributes.value || ''; 19 delete attributes.value; 20 delete attributes.multi; 21 delete attributes.multiline; 22 delete attributes.multiLine; 23 return this.element('textarea', attributes); 24 } 25 } 26 27 Html.prototype.checkBox = function(attributes) { 28 attributes.type = 'checkbox'; 29 attributes.checked = attributes.checked ? 1 : 0; 30 delete attributes.text; 31 return this.element('input', attributes); 32 } 33 34 Html.prototype.radioBox = function(attributes) { 35 attributes.type = 'radio'; 36 attributes.checked = attributes.checked ? 1 : 0; 37 delete attributes.text; 38 return this.element('input', attributes); 39 } 40 41 Html.prototype.button = function(attributes) { 42 attributes.value = attributes.value || attributes.text; 43 delete attributes.text; 44 45 if (!attributes.type || !(attributes.type == 'reset' || attributes.type == 'submit' || attributes.type == 'button' || attributes.type == 'image')) { 46 attributes.type = 'button;' 47 } 48 49 return this.element('input', attributes); 50 } 51 52 Html.prototype.listBox = function(attributes) { 53 var innerHTML= ''; 54 if (attributes.options) { 55 if (_.isFunction(attributes.options)) { 56 try { 57 attributes.options = attributes.options(); 58 } 59 catch (err) { 60 attributes.options = []; 61 } 62 } 63 64 var self=this; 65 _.each(attributes.options, function(item) { 66 if (item.value && item.value == attributes.value 67 || item.text && item.text == attributes.text) { 68 item.selected = '1'; 69 } 70 innerHTML += self.element('option', item); 71 }); 72 73 attributes.text = innerHTML; 74 delete attributes.options; 75 } 76 77 return this.element('select', attributes); 78 } 79 80 Html.prototype.hidden = function(attributes) { 81 attributes.type = 'hidden'; 82 attributes.value = attributes.value || attributes.text; 83 delete attributes.text; 84 return this.element('input', attributes); 85 } 86 87 Html.prototype.label = function(attributes) { 88 if (attributes.for) { 89 return this.element('label', attributes); 90 } 91 else { 92 return this.element('span', attributes); 93 } 94 } 95 96 Html.prototype.link = function(attributes) { 97 return this.element('a', attributes); 98 } 99 100 Html.prototype.actionLink = function(attributes) { 101 return this.link(attributes); 102 } 103 104 Html.prototype.image = function(attributes) { 105 return this.element('img', attributes); 106 }
7. 路由 (routes.js)
路由功能是一个完整的框架必须实现的,并且框架的强壮性同此息息相关。
但是呢,要实现一套完成的路由功能,难度确实也不低。这里,就只实现了最简单的路由功能,以解析这样的Url:
~/<Controller>/<Action>/<parameter1>/<parameter2>/...
1 var url = require('url'); 2 3 module.exports._routes=[]; 4 5 //Parse the controller name and method name and additional parameters 6 module.exports.parseControllerInfo = function(req) { 7 var controllerName = null, methodName = null, parameters = null; 8 9 //this section must be optimized in future. 10 var paths = url.parse(req.url).pathname.split('/'); 11 if (paths.length >= 1) 12 controllerName = paths[1]; 13 if (paths.length >= 2) 14 methodName = paths[2]; 15 if (paths.length >=3) { 16 parameters = paths.slice(3); 17 } 18 19 return { 20 controllerName : controllerName, 21 methodName : methodName, 22 parameters : parameters 23 } 24 }
五:模型(Model)的实现
在MVC中,Model是一个不可或缺的成分。而本文所描述的框架,在Controller和View方面做了较多的努力,但是对Model则没有太多涉及。
这意味着这个框架没能实现MVC的Model吗?当然不是。
在我理解,MVC的Model所实现的功能是:
- 后台程序将数据对象传入View中,由视图使用
- 浏览器回传的时候,自动把传入数据(post或get)打包成对象,由后台程序使用
针对上述需求,此框架是这样处理的:
- Html.renderView方法中调用了ejs.render方法来处理模板,并传入了任何可能使用的数据
- RequestHandler.handleDynamicRequest方法,把客户端传入的任何数据,打包成了一个可以直接使用的数据集合
当然这点儿Model实现是过于简单了(不过以后可以扩展嘛)。
六:应用示例
好了,以上就是本文所介绍的基于node.js的一个MVC框架的全部内容,虽然很简陋,不过确实能用。
这里做一个简单的示例来补充一下。
首先在controllers文件夹下新建一个test.js文件:
1 var _ = require('underscore'); 2 var _s = require('underscore.string'); 3 var mvc = require('mvc'); 4 5 module.exports = function(req, res) { 6 _.extend(this, mvc.controller(req, res)); 7 8 this.done = function(params, model) { 9 var data = { 10 users: [ 11 { name: 'tj' }, 12 { name: 'mape' }, 13 { name: 'guillermo' } 14 ], 15 utils: { 16 caption: function (str) { 17 return _s.capitalize(str); 18 } 19 }, 20 listOptions: function() { 21 var res=[]; 22 for (var i=0;i<10;i++) { 23 res.push({value: i+'', text: (i*10)+''}); 24 } 25 return res; 26 } 27 }; 28 _.extend(data, model); 29 this.responseView({name: 'done.htm', data : data}); 30 } 31 }
注意,第3,6,28行代码是必须的,其它的根据实际需求写。
然后在views文件夹下新建一个test文件夹,在其中新建一个done.htm文件:
1 <html> 2 <body> 3 <form method="post"> 4 <% if (model.users && model.users.length) { %> 5 <ul> 6 <% model.users.forEach(function(item){ %> 7 <li> <%= model.utils.caption(item.name)%>: <%= item.name %></li> 8 <% }) %> 9 </ul> 10 <% } %> 11 <div> 12 TextBox1: <%- html.textBox({name: "txtTest1", value: model.txtTest1}) %><br> 13 TextBox2: <%- html.textBox({name: "txtTest2", value: model.txtTest2, multi: true}) %><br> 14 CheckBox1: <%- html.checkBox({name: "chkTest1", value: "dd", checked: model.chkTest1}) %><br> 15 CheckBox2: <%- html.checkBox({id: "chkTest2", name: "chkTest2", value: "dd", checked: model.chkTest2}) %><%- html.label({for: "chkTest2", text: "Text for Check box"}) %><br> 16 DropDownList: <%- html.listBox({name: "lstTest1", value: model.lstTest1, options: [{value:1, text:"aa"},{value:2, text:"dd"}]}) %><br> 17 List Box: <%- html.listBox({name: "lstTest2", value: model.lstTest2, size: 4, options: model.listOptions}) %><br> 18 <%- html.button({type: "submit"}) %> 19 </div> 20 </form> 21 </body> 22 </html>
启动node.js:node index.js,在浏览器中输入地址:http://localhost:88/test/done
然后就可以看到如下结果:
全文完!
初次写博客,水平有限。若对同学们能有略微帮助,不甚荣幸。