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