第一步:實現一個最簡單的靜態服務器:javascript
var http = require('http'); var server = http.createServer(function(req,res) { res.writeHeader(200,{'Content-Type': 'text/plain'}); res.end('hello'); }); server.listen(9030,function() { console.log('you are listening port 9030'); });
第二步:MIME類型支持,當本地存在資源時響應200狀態碼,不存在響應404狀態嗎,默認UTF-8編碼
要讀文件,須要引入url,fs模塊,如今已經實現200,404狀態碼機制了css
var server = http.createServer(function(req,res) { var pathname= url.parse(req.url).pathname;//解析路徑 var resourcePath = 'home' + pathname;//資源路徑 if(fs.existsSync(resourcePath)) {//判斷資源是否存在,存在則讀取 fs.readFile(resourcePath,'binary',function(err,resource) { if(err) { res.writeHead(500,{'Content-Type': 'text/plain'}); res.end(); }else { res.writeHead(200, {'Content-Type': 'text/html'}); res.write(resource, "binary"); res.end(); } }) }else { res.writeHead(404,{'Content-Type': 'text/plain'}); res.write('No Found'); res.end(); } });
接下來就是支持MIME類型,由於服務器不可能知識存儲一種類型的資源。增長一個配置文件config.js,內容以下。html
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" };
而後使用path模塊的extname方法解析文件後綴命。前端
var path = require('path'); var mimeList = require('./config').types; var server = http.createServer(function(req,res) { var pathname= url.parse(req.url).pathname;//解析路徑 var resourcePath = 'home' + pathname;//資源路徑 var suffix = path.extname(pathname).slice(1);//獲取後綴 var contentType = mimeList[suffix]; if(fs.existsSync(resourcePath)) {//判斷資源是否存在,存在則讀取 fs.readFile(resourcePath,'binary',function(err,resource) { if(err) { res.writeHead(500,{'Content-Type': 'text/plain'}); res.end(); }else { res.writeHead(200, {'Content-Type': contentType}); res.write(resource, "binary"); res.end(); } }) }else { res.writeHead(404,{'Content-Type': contentType}); res.write('No Found'); res.end(); } });
到如今,已經實現了一個比較完整的靜態服務器了。那麼接下來重點來了,也就是實現前端老生常談的緩存。接下實現304緩存邏輯,在config文件下增長以下配置,設置過時時間java
exports.Expires = { maxAge: 60*60*24*365 };
增長以下代碼:node
var Expires = require('./config').Expires; var expires = new Date(); expires.setTime(expires.getTime() + Expires.maxAge * 1000); res.writeHead(200, {'Content-Type': contentType,"Expires":expires.toUTCString(), "Cache-Control": "max-age=" + Expires.maxAge });
304狀態碼:在服務器上爲全部請求的響應都添加Last-Modified頭,當瀏覽器發送第二次請求時會帶上If-Modified-Since字段,而後將該字段的值跟文件最後修改時間比較,若是同樣則不返回內容。獲取文件最後修改時間用fs.stat()方法
主要代碼以下:npm
fs.stat(resourcePath,function(err,stat) { var lastModified = stat.mtime.toUTCString(); var ifModifiedSince = "If-Modified-Since".toLowerCase(); res.setHeader("Last-Modified", lastModified); var expires = new Date(); expires.setTime(expires.getTime() + Expires.maxAge * 1000); if (req.headers[ifModifiedSince] && lastModified == req.headers[ifModifiedSince]) {//實現304邏輯 res.writeHead(304, "Not Modified"); res.end(); }else { res.writeHead(200, {'Content-Type': contentType,"Expires":expires.toUTCString(), "Cache-Control": "max-age=" + Expires.maxAge }); res.write(resource, "binary"); res.end(); } });
增長etag驗頭,nodejs生成etag要按照etag包,npm install etag
,增長代碼:json
var ifNoneMatch = req.headers['if-none-match']; if (req.headers[ifModifiedSince] && lastModified == req.headers[ifModifiedSince] || ifNoneMatch === etag(resource)) {//實現304邏輯,etag
完整代碼:瀏覽器
var server = http.createServer(function(req,res) { var pathname= url.parse(req.url).pathname;//解析路徑 var resourcePath = 'home' + pathname;//資源路徑 var suffix = path.extname(pathname).slice(1);//獲取後綴 var contentType = mimeList[suffix]; if(fs.existsSync(resourcePath)) {//判斷資源是否存在,存在則讀取 fs.readFile(resourcePath,'binary',function(err,resource) { if(err) { res.writeHead(500,{'Content-Type': 'text/plain'}); res.end(); }else { fs.stat(resourcePath,function(err,stat) { var lastModified = stat.mtime.toUTCString(); var ifModifiedSince = "If-Modified-Since".toLowerCase(); res.setHeader("Last-Modified", lastModified); var expires = new Date(); expires.setTime(expires.getTime() + Expires.maxAge * 1000); console.log(etag(resource),req.headers); var ifNoneMatch = req.headers['if-none-match']; if (req.headers[ifModifiedSince] && lastModified == req.headers[ifModifiedSince] || ifNoneMatch === etag(resource)) {//實現304邏輯 res.writeHead(304, "Not Modified"); res.end(); }else { res.writeHead(200, {'Content-Type': contentType,"Expires":expires.toUTCString(), "Cache-Control": "max-age=" + Expires.maxAge, "ETag":etag(resource) }); res.write(resource, "binary"); res.end(); } }) } }) }else { res.writeHead(404,{'Content-Type': contentType}); res.write('No Found'); res.end(); } });
開啓Gzip壓縮緩存
修改代碼以下:
var resource = fs.createReadStream(resourcePath); var acceptEncoding = req.headers['accept-encoding']; if(acceptEncoding && acceptEncoding.indexOf('gzip') != -1) {//判斷是否須要開啓Gzip res.writeHead(200, "Ok", {'Content-Encoding': 'gzip'}); resource.pipe(zlib.createGzip()).pipe(res); }else { res.writeHead(200, "Ok"); resource.pipe(res); }
最後一步,解決/../../index.html這種相對路徑請求訪問到其餘系統文件。思路:首先替換掉全部的..,而後調用path.normalize方法來處理掉不正常的/。
var resourcePath = path.join("home", path.normalize(pathname.replace(/\.\./g, "")));
到這裏基本完成一個靜態服務器了:
var http = require('http'); var url = require('url'); var fs = require('fs'); var path = require('path'); var mimeList = require('./config').types; var Expires = require('./config').Expires; var zlib = require('zlib'); var server = http.createServer(function(req,res) { var pathname= url.parse(req.url).pathname;//解析路徑 var resourcePath = path.join("home", path.normalize(pathname.replace(/\.\./g, ""))); var suffix = path.extname(pathname).slice(1);//獲取後綴 var contentType = mimeList[suffix]; if(fs.existsSync(resourcePath)) {//判斷資源是否存在,存在則讀取 fs.stat(resourcePath,function(err,stat) { var lastModified = stat.mtime.toUTCString(); var ifModifiedSince = "If-Modified-Since".toLowerCase(); var expires = new Date(); res.setHeader("Last-Modified", lastModified); res.setHeader('Content-Type',contentType); res.setHeader("Expires",expires.toUTCString()); res.setHeader("Cache-Control", "max-age=" + Expires.maxAge); expires.setTime(expires.getTime() + Expires.maxAge * 1000); if (req.headers[ifModifiedSince] && lastModified == req.headers[ifModifiedSince]) {//實現304邏輯 res.writeHead(304, "Not Modified"); res.end(); }else { var resource = fs.createReadStream(resourcePath); var acceptEncoding = req.headers['accept-encoding']; if(acceptEncoding && acceptEncoding.indexOf('gzip') != -1) {//判斷是否須要開啓Gzip res.writeHead(200, "Ok", {'Content-Encoding': 'gzip'}); resource.pipe(zlib.createGzip()).pipe(res); }else { res.writeHead(200, "Ok"); resource.pipe(res); } } }) }else { res.writeHead(404,{'Content-Type': contentType}); res.write('No Found'); res.end(); } }); server.listen(9030,function() { console.log('you are listening port 9030'); });
附上一篇不錯的文章,裏面還有更多的一些細節Nodejs實現靜態服務器,抄抄改改哈哈【滑稽】