本文提供了一個模仿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
然后就可以看到如下結果:
全文完!
初次寫博客,水平有限。若對同學們能有略微幫助,不甚榮幸。