通常对于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对于这些细节都有体现,实际应用中在使用框架带来的方便的同时,细节也是需要了解一些的。