먼저 github에 이 소스코드를 제공해주신 github와 작성자님께 감사의 말씀을 전하고 싶습니다. 오늘날의 정적 파일 서버는 어제보다 조금 더 복잡하며 많은 새로운 것을 배울 수 있습니다.
자세히 살펴보면 이번 코드에는 fs.stat 함수와 ReadStream 객체의 파이프 함수가 있다는 것을 알 수 있습니다. stat 함수는 파일 정보를 얻는 데 사용됩니다. 첫 번째 매개변수는 전달된 파일 경로이고, 두 번째 매개변수는 콜백 함수이며, 두 번째 매개변수인 stats의 속성은 파일의 기본 정보입니다. 파이프 함수는 이 읽기 가능한 스트림을 쓰기 가능한 대상 스트림에 연결하는 데 사용됩니다. 이 스트림에 전달된 데이터는 대상 스트림에 기록됩니다. 필요한 경우 스트림을 일시 중지하고 다시 시작하여 소스 및 대상 스트림의 동기화가 유지됩니다.
이 정적 파일 서버의 개선점은 Last-Modified 및 If-Modified-Since 헤더를 사용한다는 것입니다. 이를 통해 이미 존재하는 파일을 브라우저에 반환할 필요가 없습니다. 그런데 브라우저 요청 리소스의 압축 방법에 따라 리소스를 gzip 또는 deflate 압축을 위해 리소스로 반환할 수 있습니다.
var PORT = 8000; var http = require("http"); var url = require("url"); var fs = require("fs"); var path = require("path"); var mime = require("./mime").types; var config = require("./config"); var zlib = require("zlib"); var server = http.createServer(function(request, response) { response.setHeader("Server", "Node/V5"); var pathname = url.parse(request.url).pathname; console.log("url = " + pathname); if (pathname.slice(-1) === "/") { pathname = pathname + config.Welcome.file; } var realPath = __dirname + "/" + path.join("assets", path.normalize(pathname.replace(/\.\./g, ""))); console.log("realPath = " + realPath); var pathHandle = function (realPath) { fs.stat(realPath, function (err, stats) { if (err) { response.writeHead(404, "Not Found", {'Content-Type': 'text/plain'}); response.write("stats = " + stats); response.write("This request URL " + pathname + " was not found on this server."); response.end(); } else { if (stats.isDirectory()) { realPath = path.join(realPath, "/", config.Welcome.file); pathHandle(realPath); } else { var ext = path.extname(realPath); ext = ext ? ext.slice(1) : 'unknown'; var contentType = mime[ext] || "text/plain"; response.setHeader("Content-Type", contentType); //获得文件的修改时间 var lastModified = stats.mtime.toUTCString(); var ifModifiedSince = "If-Modified-Since".toLowerCase(); //设置Last-Modified //服务器给浏览器返回文件最后一次修改时间Last-Modified response.setHeader("Last-Modified", lastModified); if (ext.match(config.Expires.fileMatch)) { var expires = new Date(); expires.setTime(expires.getTime() + config.Expires.maxAge * 1000); response.setHeader("Expires", expires.toUTCString()); response.setHeader("Cache-Control", "max-age=" + config.Expires.maxAge); } //服务器接收浏览器发送过来的If-Modified-Since报文头 //日期相同表示该资源没有变化则返回304 //告诉浏览器该资源你已经有了,不需要再请求了 if (request.headers[ifModifiedSince] && lastModified == request.headers[ifModifiedSince]) { response.writeHead(304, "Not Modified"); response.end(); } else { var raw = fs.createReadStream(realPath); var acceptEncoding = request.headers['accept-encoding'] || ""; var matched = ext.match(config.Compress.match); if (matched && acceptEncoding.match(/\bgzip\b/)) { response.writeHead(200, "Ok", {'Content-Encoding': 'gzip'}); raw.pipe(zlib.createGzip()).pipe(response); } else if (matched && acceptEncoding.match(/\bdeflate\b/)) { response.writeHead(200, "Ok", {'Content-Encoding': 'deflate'}); raw.pipe(zlib.createDeflate()).pipe(response); } else { response.writeHead(200, "Ok"); raw.pipe(response); } } } } }); }; pathHandle(realPath); }); server.listen(PORT); console.log("Server runing at port: " + PORT + ".");
만료 필드는 웹페이지나 URL 주소가 브라우저에 의해 더 이상 캐시되지 않는 시간을 선언합니다. 이 시간이 초과되면 브라우저는 원래 서버에 접속해야 합니다. 여기서 만료 시간은 1년으로 설정됩니다.
exports.Expires = { fileMatch: /^(gif|png|jpg|js|css)$/ig, maxAge: 60*60*24*365 }; exports.Compress = { match: /css|js|html/ig }; exports.Welcome = { file: "index.html" };
다양한 리소스의 종류를 열거하고 확장자에 따라 Content-Type을 설정합니다.
exports.types = { "css": "text/css", "gif": "image/gif", "html": "text/html", "ico": "image/x-icon", "jpeg": "image/jpeg", "jpg": "image/jpeg", "js": "text/javascript", "json": "application/json", "pdf": "application/pdf", "png": "image/png", "svg": "image/svg+xml", "swf": "application/x-shockwave-flash", "tiff": "image/tiff", "txt": "text/plain", "wav": "audio/x-wav", "wma": "audio/x-ms-wma", "wmv": "video/x-ms-wmv", "xml": "text/xml" };