之前写过一篇文章,简单介绍了一个基于Node.js的静态文件服务器。那时还只是个人兴趣。最近又有了关于服务器的新的需求,我就想花点时间,好好研究一下。所以把之前的代码拿出来重构了一番,整体代码变得干净很多。
首先最新Node.js是支持generator的,所谓generator,就是javascript中的协程(半协程),不过功能稍弱,仅仅是为了解决js中凶名赫赫的callback hell而诞生的。这里我并没有使用generator,而是使用promise(饭要一口一口吃,先弄明白promise再去学习generator)。promise并不是新的语法,而是一种书写方式。最出名的实现是Q。
关于Q的学习资料可以看这里,非常清楚。
server.js的代码:
'use strict';
var CONFIG = {
'host': '127.0.0.1',
'port': 9527,
'site_base': './site',
'file_expiry_time': 0, // HTTP cache expiry time, minutes
'directory_listing': true
};
var MIME_TYPES = {
'.txt': 'text/plain',
'.md': 'text/plain',
'': 'text/plain',
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.jpg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.zip': 'text/plain',
'.cfg': 'text/plain'
};
var EXPIRY_TIME = (CONFIG.file_expiry_time * 60).toString();
var http = require('http');
var Path = require('path');
var Crypto = require('crypto');
var Custard = require('./custard/custard');
var Q = require('q');
var fs = require('./filesystem')
// An object representing a server response
function ResponseObject( metadata ){
this.status = metadata.status || 200;
this.data = metadata.data || false;
this.type = metadata.type || false;
}
ResponseObject.prototype.getEtag = function (){
var hash = Crypto.createHash( 'md5' );
hash.update( this.data );
return hash.digest( 'hex' );
};
function getFileList(files, url, callback, error) {
var template = new Custard;
var full_path = CONFIG.site_base + url;
var i = 0;
template.addTagSet('h', require('./custard/templates/tags/html'));
template.addTagSet('c', {
'title': 'Index of ' + url,
'file_list': function (h) {
var items = [];
var stats;
for (i = 0; i < files.length; i += 1) {
stats = fs.statSync(full_path + files[i]);
if (stats.isDirectory()) {
files[i] += '/';
}
items.push(h.el('li', [
h.el('a', {'href': url + files[i]}, files[i])
]));
}
return items;
}
});
Q.nbind(template.render, template)
("\
h.doctype('html5'),\
h.html([\
h.head([\
h.el('title', c.title),\
]),\
h.body([\
h.el('h1', c.title),\
h.el('ul', c.file_list(h))\
])\
])"
).then(callback, error);
}
// Filter server requests by type
function handleRequest(url){
// hack fix version没有扩展名,但是是一个文本文件]
var url = url;
var deferred = Q.defer();
if ( Path.extname( url ) === '' && url.indexOf('version') == -1 ) {
// 路径
var full_path = CONFIG.site_base + url;
if (!CONFIG.directory_listing) {
// Forbidden
deferred.resolve(new ResponseObject({'status': 403}))
return deferred.Promise;
}
fs.exists(full_path)
.then(function() {
return fs.readdir(full_path)
}, function() {
deferred.resolve(new ResponseObject({'status': 404}))
})
.then(function(files) {
getFileList(files, url, function(html) {
deferred.resolve(new ResponseObject({'data': new Buffer(html), 'type': 'text/html'}));
}, function(error) {
deferred.resolve(new ResponseObject({'data': error.stack, 'status': 500}));
})
}, function(error) {
// Internal error
deferred.resolve(new ResponseObject({'data': error.stack, 'status': 500}));
})
} else {
// 文件
var path = CONFIG.site_base + url;
fs.exists(path)
.then(function() {
return fs.readFile(path)
}, function() {
deferred.resolve(new ResponseObject({'status': 404}));
})
.then(function(data){
deferred.resolve(new ResponseObject( {'data': new Buffer( data ), 'type': MIME_TYPES[Path.extname(path)]}));
}, function(error) {
deferred.resolve(new ResponseObject({'data': error.stack, 'status': 500}))
})
}
return deferred.promise;;
}
function parseRange (str, size) {
if (str.indexOf(",") != -1) {
return;
}
str = str.replace("bytes=", "");
var range = str.split("-"),
start = parseInt(range[0], 10),
end = parseInt(range[1], 10);
// Case: -100
if (isNaN(start)) {
start = size - end;
end = size - 1;
// Case: 100-
} else if (isNaN(end)) {
end = size - 1;
}
// Invalid
if (isNaN(start) || isNaN(end) || start > end || end > size) {
return;
}
return {
start: start,
end: end
};
};
var compressHandle = function (raw, matched, statusCode, reasonPhrase) {
var stream = raw;
var acceptEncoding = request.headers['accept-encoding'] || "";
if (matched && acceptEncoding.match(/\bgzip\b/)) {
response.setHeader("Content-Encoding", "gzip");
stream = raw.pipe(zlib.createGzip());
} else if (matched && acceptEncoding.match(/\bdeflate\b/)) {
response.setHeader("Content-Encoding", "deflate");
stream = raw.pipe(zlib.createDeflate());
}
response.writeHead(statusCode, reasonPhrase);
stream.pipe(response);
};
// Start server
http.createServer(function(request, response) {
var headers;
var etag;
if ( request.method === 'GET' ){
// Get response object
handleRequest( request.url).then(function (response_object) {
if (!response_object || ! response_object.data || response_object.data.length <= 0 ) {
// 无文件内容
response.writeHead(response_object.status);
response.end();
return;
}
etag = response_object.getEtag();
if ( request.headers.hasOwnProperty('if-none-match') && request.headers['if-none-match'] === etag ){
// Not Modified
response.writeHead( 304 );
response.end();
return;
}
var fileFullSize = response_object.data.length;
if (request.headers["range"]) {
// 如果有range
var range = parseRange(request.headers["range"], fileFullSize);
if (range) {
var raw = fs.createReadStream(CONFIG.site_base + request.url, {
"start": range.start,
"end": range.end
});
//console.log(raw);
headers = {
'Accept-Ranges': 'bytes',
'Content-Type': response_object.type,
'Content-Length' : (range.end - range.start + 1),
'Cache-Control' : 'max-age=' + EXPIRY_TIME,
'Content-Range' : "bytes " + range.start + "-" + range.end + "/" + fileFullSize,
'ETag' : etag
};
console.log("range " + range.start + "-" + range.end);
response.writeHead( response_object.status, headers );
//response.end( response_object.data );
raw.pipe(response);
//raw = "";
//response.end( raw );
} else {
console.log("range format error");
// range格式错误
response.removeHeader("Content-Length");
response.writeHead(416, "Request Range Not Satisfiable");
response.end();
}
} else {
// 没有range,全文件
headers = {
'Accept-Ranges': 'bytes',
'Content-Type': response_object.type,
'Content-Length' : response_object.data.length,
'Cache-Control' : 'max-age=' + EXPIRY_TIME,
'ETag' : etag
};
response.writeHead( response_object.status, headers );
response.end( response_object.data );
}
} );
} else if ( request.method == 'HEAD') {
handleRequest(request.url).then(function(response_object){
if (!response_object || !response_object.data || response_object.data.length <= 0 ) {
response.writeHead(response_object.status);
response.end();
return;
}
etag = response_object.getEtag();
if ( request.headers.hasOwnProperty('if-none-match') && request.headers['if-none-match'] === etag ){
// Not Modified
response.writeHead( 304 );
response.end();
return;
}
headers = {
'Content-Type': response_object.type,
'Content-Length' : response_object.data.length,
'Cache-Control' : 'max-age=' + EXPIRY_TIME,
'ETag' : etag
};
response.writeHead( response_object.status, headers );
response.end();
} );
} else {
// Forbidden
response.writeHead(403);
response.end();
}
} ).listen( CONFIG.port, CONFIG.host );
console.log( 'Site Online : http://' + CONFIG.host + ':' + CONFIG.port.toString() + '/' );
filesystem.js的代码(在server.js中有用到,只是使用Q简单的封装了下node.js的异步文件操作):
var Q = require('q')
var fs = require('fs')
// 使用Q封装回调形式的文件操作函数
var fs_readFile = Q.nfbind(fs.readFile);
var fs_readdir = Q.nfbind(fs.readdir);
var fs_stat = Q.nfbind(fs.stat);
function exists(path) {
var defer = Q.defer();
fs.exists(path, function(exists) {
if (exists) {
defer.resolve(exists);
} else {
defer.reject(exists);
}
});
return defer.promise;
}
function readFile(path) {
return fs_readFile(path);
}
function readdir(path) {
return fs_readdir(path);
}
function stat(path) {
return fs_stat(path)
}
// 同步函数,直接调用
function statSync(path) {
return fs.statSync(path);
}
function readFileSync(path) {
return fs.readFileSync(path)
}
module.exports.exists = exists;
module.exports.readFile = readFile;
module.exports.readFileSync = readFileSync;
module.exports.readdir = readdir;
module.exports.stat = stat;
module.exports.statSync = statSync;
说实话,即便是重构后的代码,也不是非常简洁。使用Q最主要是解决多层回调的问题的。在上面的代码中,其实没有太多的多层调用,所以体现的不是非常明显,不过项目大了,会有显著的效果。
即便使用Q之后,调试起来还是比较麻烦,因为我们无法清晰的知道这个函数是谁,在什么时候调用的,也就是说,很多时候我们获取不到调用堆栈(可以获取到,但是获取到的信息几乎无意义,因为函数都是在Q的task中调用的)。
关于Q封装Node.js的异步函数,这里需要注意一下filesystem.js中的exists()函数。Q.nfbind做的事情其实就是exists中所做的。函数第一个参数必须是error,第二个参数必须是data,这样的函数才能够直接使用Q.nfbind封装,像fs.exits函数,只有一个参数,所以无法使用Q.nfbind封装。