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對於這些細節都有體現,實際應用中在使用框架帶來的方便的同時,細節也是需要了解一些的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章