之前写过一篇文章,简单介绍了一个基于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封装。