node.js 后端框架設計構想
我打算把我的后端的框架定位為建站框架,本文是我的一些思路與初步實踐。如果園子里有做過后端框架的高手(不限語言),也請指教一下。以下是大概的流程。
后端的核心文件mass.js包含批量創建與刪除文件夾,MD5加密,類型識別與模塊加載等功能。現在網站名與網站的路徑也還是混淆在里面,以后會獨立到一個配置文件中。只要運行node mass.js這命令就立即從模板文件中構建一個樣板網站出來。下面就是它建站的最主要代碼:
//--------開始創建網站--------- //你想建立的網站的名字(請修正這里) mass.appname = "jslouvre" ; //在哪個目錄下建立網站(請修正這里) mass.approot = process.cwd(); //用于修正路徑的方法,可以傳N個參數 mass.adjustPath = function (){ [].unshift.call(arguments,mass.approot, mass.appname); return require( "path" ).join.apply( null ,arguments) } var dir =
mass.adjustPath( "" ) // mass.rmdirSync(dir);//...... mass.require( "http,fs,path,scaffold,intercepters" , function (http,fs,path,scaffold,intercepters){ mass.log( "<CODE style=" COLOR:
blue ">=========================</CODE>" , true ) if (path.existsSync(dir)){ mass.log( "<CODE
style=" COLOR: red ">此網站已存在</CODE>" , true ); } else { fs.mkdir(dir,0755) mass.log( "<CODE
style=" COLOR: green ">開始利用內部模板建立您的網站……</CODE>" , true ); } global.mapper =
scaffold(dir); //取得路由系統 http.createServer( function (req, res) { var arr = intercepters.concat(); req.on( "err500" , function (err){ res.writeHead(500, { "Content-Type" : "text/html" }); var html = fs.readFileSync(mass.adjustPath( "public/500.html" )) var arr = [] for ( var i in err){ arr.push( "<LI>" +i+ " : " +err[i]+ " " ) } res.write((html+ "" ).replace( "{{url}}" ,arr.join( "" ))); res.end(); }); req.on( "next_intercepter" , function (){ try { var next = arr.shift(); next
&& next.apply( null ,arguments) } catch (err){ req.emit( "err500" ,err); } }); req.emit( "next_intercepter" ,req, res); }).listen(8888); console.log( "start server in 8888 port" ) });
|
只要運行mass.js,它會根據appname與approot判定目標路徑是否存在此網站,沒有就創建相應文件夾 fs.mkdir(dir,0755)。但更多的文件夾與文件是由scaffold.js完成的。scaffold里面個文件夾列表,用于讓程序從templates把相應的文件夾拷貝到網站的路徑下,并建立505.html, 404.html, favicon.ico, routes.js等文件。其中最重頭的是routes,它是用來定義路由規則。
//routes.js //最重要的部分,根據它生成controller, action, model, views mass.define( "routes" , function (){ return function (map){ //方法路由 //
map.get('/','site#index'); //
map.get('/get_comments/:post_id','site#get_comments'); //
map.post('/add_comment','site#add_comment'); // //資源路由 //
map.resources('posts'); //
map.resources('users'); //
map.get('/view/:post_name','site#view_post'); //
map.get('/rss','site#rss'); //
map.resources('posts', {path: 'articles', as: 'stories'}); //嵌套路由 //
map.resources('posts', function (post) { //
post.resources('users'); // }); //命名空間路由 map.namespace( "tests" , function (tests){ tests.resources( 'comments' ); }) // map.resources('users', { // only: ['index', 'show'] // }); // // map.resources('users', { // except: ['create', 'destroy'] // }); // map.resources('users', function (user) { // user.get('avatar', 'users#avatar'); // }); // map.root("home#index") } }); |
上面就是routes.js的所有內容。允許建立五種路由:根路由,資源路由,方法路由(get,delete,put,post),命名空間路由,嵌套路由。其實它們統統都會歸化為資源路由,每個URL都對應一個控制器與其下的action。它會調用router.js,讓里面的Router實例mapper調用router.js里面的內容,然后返回mapper。
//scaffold.js var routes_url = mass.adjustPath( 'config/routes.js' ), action_url = "app/controllers/" , view_url = "app/views/" , mapper = new Router mass.require( "routes(" +routes_url+ ")" , function (fn){ //讀取routes.js配置文件 fn(mapper) }); //這里省掉,一會兒解說 return mapper; |
Router實例mapper在routes運行完畢后,那么它的幾個屬性就會添加了N多成員與元素,我們再利用它來進一步構建我們的控制器,視圖與模型。。。
//如
this.controllers = {};現在變為 { comments: { actions: [ 'index' , 'create' , 'new' , 'edit' , 'destroy' , 'update' , 'show' ], views: [ 'index' , 'new' , 'edit' , 'show' ], namespace: 'tests' } } // this.GET =
[];現在變為 [ { controller: 'comments' , action: 'index' , method: 'GET' , namespace: '/tests/' , url: '/tests/comments.:format?' , helper: 'tests_comments' , matcher: /^\/tests\/comments$/i }, { controller: 'comments' , action: 'new' , method: 'GET' , namespace: '/tests/' , url: '/tests/comments/new.:format?' , helper: 'new_tests_comments' , matcher: /^\/tests\/comments\/ new $/i }, { controller: 'comments' , action: 'edit' , method: 'GET' , namespace: '/tests/' , url: '/tests/comments/:id/edit.:format?' , helper: 'edit_tests_comment' , matcher: /^\/tests\/comments\/\d+\/edit$/i }, { controller: 'comments' , action: 'show' , method: 'GET' , namespace: '/tests/' , url: '/tests/comments/:id.:format?' , helper: 'tests_comment' , matcher: /^\/tests\/comments\/\d+$/i }
] |
mapper有四個數組屬性,GET,POST,DELETE,PUT,我稱之為匹配棧,這些數組的元素都是一個個對象,對象都有一個matcher的正則屬性,就是用來匹配請求過來的URL的pathname屬性,當然首先我們先取得其method,讓相應的匹配棧去處理它。
現在手腳架scaffold.js還很簡鄙,以后它會結合熱部署功能,當用戶修改routes.js或其他配置文件時,它將會自動生成更多的視圖與控制器等等。
然后我們就啟動服務器了,由于req是EventEmitter的實例,因此我們可以隨意在上面綁定自定義事件,這里有兩個事件next_intercepter與err500。err500就不用說了,next_intercepter是用來啟動攔截器群集。這里我們只需要啟動第一個。它在回調中會自動啟動下一個。這些攔截器是由intercepters.js 統一加載的。
//intercepters.js mass.intercepter = function (fn){ //攔截器的外殼 return function (req, res, err){ if (err ){ req.emit( "next_intercepter" , req, res,
err); } else if (fn(req,res) === true ){ req.emit( "next_intercepter" , req, res) } } } var deps = [ "mime" , "postData" , "query" , "methodOverride" , "json" , "favicon" , "matcher" , "handle404" ]; //"more", mass.define( "intercepters" ,
deps.map( function (str){ return "intercepters/" +str }).join( "," ), function (){ console.log( "取得一系列欄截器" ); return [].slice.call(arguments,0) }); |
每個攔截器都會對原始數據進行處理,并決定是繼續啟用下一個攔截器。比如mime攔截器:
mass.define( "intercepters/mime" , function (){ console.log( "本模塊用于取得MIME,并作為request.mime而存在" ); return mass.intercepter( function (req, res){ console.log( "進入MIME回調" ); var str = req.headers[ 'content-type' ] || '' ; req.mime =
str.split( ';' )[0]; return true ; }) }) |
postData攔截器
mass.define( "intercepters/postData" , "querystring" , function (qs){ console.log( "本模塊用于取得POST請求過來的數據,并作為request.body而存在" ); return mass.intercepter( function (req,res){ console.log( "進入postData回調" ); req.body = req.body ||
{}; if ( req._body || /GET|HEAD/.test(req.method) || 'application/x-www-form-urlencoded' !== req.mime ){ return true ; } var buf = '' ; req.setEncoding( 'utf8' ); function buildBuffer(chunk){ buf += chunk } req.on( 'data' , buildBuffer); req.once( 'end' , function (){ try { if (buf != "" ){ req.body =
qs.parse(buf); req._body = true ; } req.emit( "next_intercepter" ,req,res) } catch (err){ req.emit( "next_intercepter" ,req,res,err) }finally{ req.removeListener( "data" ,buildBuffer) } }) }); }); |
query攔截器
mass.define( "intercepters/query" , "querystring,url" , function (qs,URL){ console.log( "本模塊用于取得URL的參數并轉為一個對象,作為request.query而存在" ); return mass.intercepter( function (req, res){ req.query =
~req.url.indexOf( '?' ) ?
qs.parse(URL.parse(req.url).query) : {}; return true ; }) }) |
methodOverride攔截器
mass.define( "intercepters/methodOverride" , function (){ console.log( "本模塊用于校正method屬性" ); var methods = { "PUT" : "PUT" , "DELETE" : "DELETE" }, method = mass.configs.method || "_method" ; return mass.intercepter( function (req, res){ req.originalMethod =
req.method; var defaultMethod = req.method === "HEAD" ? "GET" : req.method; var _method = req.body ? req.body[method] :
req.headers[ 'x-http-method-override' ] _method = (_method || "" ).toUpperCase(); req.method =
methods[_method] || defaultMethod; if (req.body){ delete req.body[method]; } return true ; }) }) |
json攔截器
mass.define( "intercepters/json" , function (){ console.log( "本模塊處理前端發過來的JSON數據" ); return mass.intercepter( function (req, res, err){ req.body = req.body ||
{}; if (req._body || 'GET' == req.method || !~req.mime.indexOf( "json" )){ console.log( "進入json回調" ) return true ; } else { var buf = '' ; req.setEncoding( 'utf8' ); function buildBuffer(chunk){ buf += chunk; } req.on( 'data' , buildBuffer); req.once( 'end' , function (){ try { req.body =
JSON.parse(buf); req._body = true ; req.emit( "next_intercepter" ,req,res); } catch (err){ err.status =
400; req.emit( "next_intercepter" ,req,res,err); }finally{ req.removeListener( "data" ,buildBuffer); } }); } }) }) |
而在這么多攔截器中,最重要的是matcher攔截器,它進入框架MVC系統的入口。把原始請求的pathname取出來,然后通過正則匹配它,只要一個符合就停下來,然后加載對應的控制器文件,調用相應的action處理請求!
mass.define( "intercepters/matcher" , "url" , function (URL){ console.log( "用于匹配請求過來的回調" ) return mass.intercepter( function (req,res){ console.log( "進入matcher回調" ); var pathname = URL.parse(req.url).pathname, is404 = true ,method = req.method, arr =
mapper[method]; for ( var i =0, obj; obj = arr[i++];){ if (obj.matcher.test(pathname)){ is404 = false var url = mass.adjustPath( "app/controllers/" ,obj.namespace, obj.controller+ "_controller.js" ) mass.require(obj.controller+ "_controller(" +url + ")" , function (object){ object[obj.action](req,res); //進入控制器的action!!! console.log(obj.action) }, function (){ var err = new Error; err.statusCode = 404 req.emit( "next_intercepter" ,req,res,err); }) break ; } } if (is404){ var err = new Error; err.statusCode = 404 req.emit( "next_intercepter" ,req,res,err); } }) }) |
最后殿后的是handle404攔截器:
mass.define( "intercepters/handle404" , "fs,path" , function (fs){ console.log( "本模塊用于處理404錯誤" ); return function (req, res, err){ console.log( "進入handle404回調" ); var accept = req.headers.accept || '' ; if (~accept.indexOf( 'html' )) { res.writeHead(404, { "Content-Type" : "text/html" }); var html = fs.readFileSync(mass.adjustPath( "public/404.html" )) res.write((html+ "" ).replace( "{{url}}" ,req.url)); res.end(); } else if (~accept.indexOf( 'json' )) { //json var error = { message:
err.message, stack: err.stack }; for ( var prop in err) error[prop] = err[prop]; var json = JSON.stringify({ error: error }); res.setHeader( 'Content-Type' , 'application/json' ); res.end(json); // plain text } else { res.writeHead(res.statusCode, { 'Content-Type' : 'text/plain' }); res.end(err.stack); } } }) |
再回過頭來看控制器部分,從模板中生成的controller非常簡單:
mass.define( "comments_controller" , function (){ return { "index" : function (){}, "create" : function (){}, "new" : function (){}, "edit" : function (){}, "destroy" : function (){}, "update" : function (){}, "show" : function (){} } }); |
因此你需要動手改到其可用,如
"show" : function (req,res){ res.writeHead(200, { "Content-Type" : "text/html" }); var html =
fs.readFileSync(mass.adjustPath( "app/views/tests/show.html" )) res.write(html); res.end(); } |
以后會判定action的結果自動調用視圖。
當然現在框架還很簡單,只用了半天時間而已。它必須支持ORM與靜態文件緩存才行。此外還有cookie,session等支持,這些做成一個攔截器就行了。
總結如下:
- 判定網站是否存在,沒有通過手腳架構建一個
- 讀取routes等配置文件,生成MVC系統所需要的控制器,視圖與模型。
- 通過熱部署功能,監視用戶對配置文件的修改,進一步智能生成需要控制器,視圖與模型。
- 通過一系列攔截器處理請來,直到matcher攔截器里面進入MVC系統,這時通過模型操作數據庫,渲染頁面。攔截器群集的應用大大提高應用的伸縮性。現在還沒有來得及得node.js的多線程,可能這里面能發掘出許多好東西呢。
相關代碼我稍晚會上傳到github中。。。
基本就是這樣,希望大家踴躍參與討論。