NodeJSWeb应用

通常对于Web应用而言,比较普遍的需求如下:

  • 判断请求方法
  • 解析URL
  • 解析querystring
  • 解析Cookie/Session
  • 认证
  • 处理表单数据/处理querystring
  • 文件的处理

1. 请求方法,req.method

2. 解析路由,req.url,针对controller/action这种路由处理如下:

function(req, res){
    var pathname = url.parse(req.url).pathname;
    var paths = pathname.split('/');
    var controller = paths[1] || 'index';
    var action = paths[2] || 'index';
    var args = paths.slice(3);
    if(handles[controller] && handles[controller][action]){
        handles[controller][action].apply(null, [req, res].concat(args));
    }else{
        res.writeHead(500);
        res.end('Server Error');
    }
}

handles.index = {};
handles.index.index = function(req, res. foo, bar){
    res.writeHead(200);
    res.end(foo);
}

3. 解析querystring

// method1
var url = require('url');
var queryString = require('querystring');
var query = queryString.parse(url.parse(req.url).query);

// method2
var query = url.parse(req.url, true).query;

结果将被解析为JSON对象,如果querystring中key值出现多次,则会解析为数组,再应用解析出来的结果时一定要判断数据类型。

4. 解析Cookie

在解析之前先来扫盲一下,Cookie是什么,Session是什么,具体概念上网查一大把,这里简单的说明一下。

首先Http协议是无状态的协议,但是实际应用中我们是希望它有状态的也就是能记录当前用户是谁,做了什么事儿等等,所以为了解决Http无状态的的这个问题我们可以应用Cookie,Cookie可以理解为在本地存储的记录文件,因为它存储在本地也就是客户端,很容易被人修改所以是不安全的,继而我们可以应用Session来加强安全性,因为Session是存储在服务端的。那么实际应用中应该怎么使用那,所谓Seesion就是键值对,有对应的key也就是SessionId,而SeesionId对应的就具体的数据,所以应用中我们是把SessionId存储在Cookie中,在请求中附待Cookie继而维护Http的状态。

Cookie的解析如下:

// Cookie解析方法
var parseCookie = function(cookie){
    var cookies = {};
    if(!cookie){
        return cookies;
    }
    var list = cookie.split(';');
    for(var i = 0; i < list.length; i++){
        var pair = list[i].split('=');
        cookies[pair[0].trim()] = pair[1];
    }
    return cookies;
}

// 应用
function(req, res){
    req.cookies = parseCookie(req.headers.cookie);
    handle(req, res);
}

var handle = function(req, res){
    res.writeHead(200);
    if(!req.cookies.isVisit){
        res.end('welcome');
    }else{
        // TODO=>
    }
}

            Response中Cookie是设置在’Set-Cookie‘字段是上的,Response中Cookie如下:

var serializeCookie = function(name, val, cookieObj){
    var pairs = [name + '=' + encode(val)];
    cookieObj = cookieObj || {};
    
    if(cookieObj.maxAge) pairs.push('Max-Age=' + cookieObj.maxAge);
    if(cookieObj.domain) pairs.push('Domain=' + cookieObj.domain);
    if(cookieObj.path) pairs.push('Path=' + cookieObj.path);
    if(cookieObj.expires) pairs.push('Expires=' + cookieObj.expires.toUTCString());
    if(cookieObj.httpOnly) pairs.push('HttpOnly');
    if(opt.secure) pairs.push('Secure');
    
    return pairs.join('; ');
}

var handle = function(req, res){
    if(!req.cookies.isVisit){
        res.setHeader('Set-Cookie', serialize('isVisit', '1'));
        res.writeHead(200);
        res.end('welcome');
    }else{
        res.writeHead(200);
        res.end('hello');
    }
}

Session的使用如下:

第一种比较主流的方式基于Cookie

// 生成Session
var sessions = {};
var key = 'session_id';
var expires = 20*60*1000;

var generate = function(){
    var session = {};
    session.id = (new Date()).getTime() + Math.random();
    session.cookie = {
        expire: (new Date()).getTime() + expires;
    };
    sessions[session.id] = session;
    return session;
}

// 与Cookie结合使用
function(req, res){
    var id = req.cookies[key];
    if(!id){
        var session = generate();
    }else{
        var session = session[id];
        if(session){
            if(session.cookie.expire > (new Date().getTime()){
                // 更新expires
                session.cookie.expire = (new Date()).getTime() + expires;
                req.session = session;
            }else{
                // 超时了,删除旧的数据,并重新生成
                delete sessions[id];
                req.session = generate();
            }
        }else{
            // 如果session过期或者id不对,重新生成session
            req.session = generate();
        }
    }
    handle(req, res);
}

// 重写writeHead方法
var writeHead = res.writeHead;
res.writeHead = function(){
    var cookies = res.getHeader('Set-Cookie');
    var session = serialize('Set-Cookie', req.session.id);
    cookies = Array.isArray(cookies) ? cookies.contact(session) : [cookies, session];
    res.setHeader('Set-Cookie', cookies);
    return writeHead.apply(this, arguments);
}

var handle = function(req, res){
    if(!req.session.isVisit){
        res.session.isVisit = true;
        res.writeHead(200);
        res.end('welcome');
    }else{
        res.writeHead(200);
        res.end('hello');
    }
}

第二种方式基于querystring

var getURL = function(urlParam, key, value){
    var obj = url.parse(urlParam, true);
    obj.query[key] = value;
    return url.format(obj);
}

function(req, res){
    var redirect = function(url){
        res.setHeader('Location', url);
        res.writeHead(302);
        res.end();
    }

    var id = req.query[key];
    if(!id){
        var session = generate();
        redirect(getURL(req.url, key, session.id));
    }else{
        var session = session[id];
        if(session){
            if(session.cookie.expire > (new Date()).getTime()){
                // 更新超时时间
                session.cookie.expire = (new Date()).geTime() + expires;
                req.session = session;
                handle(req, res);
            }else{
                // 超时了,删除旧的数据,并重新生成
                delete sessions[id];
                var session = generate();
                redirect(getURL(req.url, key, session.id));
            }
        }else{
            // 如果session过期或者口令不对,重新生成session
            var session = generate();
            redirect(getURL(req.url, key, session.id));
        }
    }
}

Session的安全解决方案,因为SessionId保存在客户端中所以还是很容易被修改,为了避免被修改我们应该采用加密的方式,比较稳妥的做法是用私钥加密SessionId和客户端的某种独有信息进行签名,再避免掉XSS脚本攻击。

签名的基本方法如下:

// 将值通过私钥签名,由.分割原值与签名
var sign = function(val, primaryKey){
    return val + '.' + crypto.createHmac('sha256', primaryKey)
          .update(val).digest('base64').replace(/\=+$/, '');
}

// Response时
var val = sign(req.sessionID, secret);
res.setHeader('Set-Cookie', cookie.serialize(key, val));

// 接受request的处理,取出sessionID进行签名,对比签名的值
var unsign = function(val, primaryKey){
    var str = val.slice(0, val.lastIndexOf('.'));
    return sign(str, primaryKey) == val ? str : false;
}

Post请求中比较常见格式数据的处理如下:

// Common
var hasBody = function(req){
    return 'transfer-encoding' in req.headers || 'content-length' in req.headers;
}
function(req, res){
    if(hasBody(req)){
        var buffers = [];
        req.on('data', function(chunk){
            buffers.push(chunk);
        });
        req.on('end', function(){
            req.rawBody = buffer.concat(buffers).toString();
            handle(req, res);
        });
    }else{
        handle(req, res);
    }
}

// Form data
var handle = function(req, res){
    if(req.headers['content-type'] == 'application/x-www-form-urlencoded'){
        req.body = querystring.parse(req.rawBody);
    }
    // TODO=>logic
}

// JSON
var mime = function(req){
    var str = req.headers['content-type'] || '';
    return str.split(';')[0];
}
var handle = function(req, res){
    if(mime(req) === 'application/json'){
        try{
            req.body = JSON.parse(req.rawBody);
            }catch(e){
                res.writeHead(400);
                res.end('Invalid JSON');
                return;
            }
    }
    // TODO=>logic
}

// XML
var xml2js = require('xml2js');
var handle = function(req, res){
    if(mime(req) ==='application/xml'){
        xml2js.parseString(req.rawBody, function(err, xml){
            if(err){
                res.writeHead(400);
                res.end('Invalid XML');
                return;
            }    
            req.body = xml;
            // TODO=>logic
        }
    }
}

// File update  Content-Type: multipart/form-data; boundary = xxx
var formidable = require('formidable');
function(req, res){
    if(hasBody(req)){
        if(mime(req) === 'mutipart/form-data'){
            var form = new fromidable.IncomingForm();
            form.parse(req, function(err, fields, files){
                req.body = fields;
                req.files = fields;
                handle(req, res);
            });
        }
    }else{
        handle(req, res);
    }
}

5. 中间组间,使开发者更多的关注在逻辑上儿不是底层细节上

// middleware1
var queryString = function(req, res, next){
    req.query = url.parse(req.url, true).query;
    next();
}
// middleware2
var cookie = function(req, res, next){
    var cookie = req.headers.cookie;
    var cookies = {};
    if(cookie){
        var list = cookie.split(';');
        for(var i = 0; i < list.length; i++){
            var pair = list[i].split('=');
            cookies[pair[0].trim()] = pair[1];
        }
    }
    
    req.cookies = cookies;
    next();
}

app.use = function(path){
    var handle;
    if(typeof path === 'string'){
        handle = {
            path: pathRegexp(path);
            stack: Array.prototype.slice.call(arguments, 1);
        };
    }else{
        handle = {
            path: pathRegexp('/');
            stack: Array.prototype.slice.call(arguments, 0);
        };
    }
    routes.all.push(handle);
}

var match = function(pathname, routes){
    var stacks = [];
    for(var i = 0; i < routes.length; i++){
        var reg = route.path.regexp;
        var matched = reg.exec(pathname);
        if(matched){
            stacks = stacks.concat(route.stack);
        }
    }
    return statcks;
}

function(req, res){
    var pathname = url.parse(req.url).pathname;
    var method = req.method.toLowerCase();
    var stacks = match(pathname, routes.all);
    if(routes.hasOwnPerperty(method)){
        stacks.concat(match(pathname, routes[method]));
    }
    if(stacks.length){
        handle(req, res, stacks);
    }else{
        handle404(req, res);
    }
}

var handle = function(req, res, stack){
    var next = function(err){
        if(err){
            return handle500(err, req, res, stack);
        }
        var middleware = stack.shift();
        if(middleware){
            try{
                middleware(req, res, next);                
            }catch(ex){
                next(err);
            }
        }
    }
    next();
}

6. 响应处理,浏览器如何处理你的响应取决于你的MIME类型,常用的响应如下:

// text/plain
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('<html><body>helloe world</body></html>');

// text/html
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('<html><body>helloe world</body></html>');

// attachement; filename="xxx" 附件下载
res.sendfile = function(filepath){
    fs.stat(filepath, funciton(err, stat){
        var stream = fs.createReadStream(filepath);
        res.setHeader('Content-Type', mine.lookup(filepath));
        res.setHeader('Content-Length', stat.size);
        res.setHeader('Content-Disposition' 'attachement;filename="' + path.basename(filepath) + '"');
        res.writeHead(200);
        stream.pipe(res);
    })
}

// application/json
res.json = function(json){
    res.setHeader('Content-Type', 'application/json');
    res.writeHead(200);
    res.end(JSON.stringify(json));
}

// 跳转
res.redirect = function(url){
    res.setHeader('Location', url);
    res.writeHead(302);
    res.end('Redirect to ' + url);
}

以上主要事面向Web的一些细节,现在成熟的Web框架如Connect,Express对于这些细节都有体现,实际应用中在使用框架带来的方便的同时,细节也是需要了解一些的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值