模仿ASP.Net MVC的node.js MVC实现


  本文提供了一个模仿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框架用到了一些第三方组件,这里提供其官方地址,若需要下载或对其有疑问,直接导航过去看:

  先具体说一下导致我产生这个想法的具体原因。

  为了学习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 }

  这个方法没什么好说的,根据数据构造一个字符串而已。

  后面我们要做的,就是一个个编写控件生成代码。(下面是一些常用控件的生成代码)

Html.elements
  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

  然后就可以看到如下结果:  

 

  全文完!


 

  初次写博客,水平有限。若对同学们能有略微帮助,不甚荣幸。

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM