模仿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