Ruby's Louvre

每天学习一点点算法

导航

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("=========================",true)
        if(path.existsSync(dir)){
            mass.log("此网站已存在",true);
        }else{
            fs.mkdir(dir,0755)
            mass.log("开始利用内部模板建立您的网站……",true);
        }
        global.mapper = scaffold(dir);//取得路由系统
        http.createServer(function(req, res) {
            var arr = intercepters.concat();
            //有关HTTP状态的解释 http://www.cnblogs.com/rubylouvre/archive/2011/05/18/2049989.html
            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("
  • "+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中。。。

    基本就是这样,希望大家踊跃参与讨论。

    posted on 2011-12-13 16:22  司徒正美  阅读(8686)  评论(6编辑  收藏  举报