搞起node.js靜態服務器並實戰前端緩存

要實現的內容大概是這樣的

  • MIME類型支持,當本地存在資源時響應200狀態碼,不存在響應404狀態嗎,默認UTF-8編碼
  • 客戶端過時時間設置爲1年
  • 靜態資源在服務器存放的根目錄是/home
  • 實現304狀態碼響應邏輯,etag簽名
  • 開啓Gzip壓縮文件
  • 儘量提升響應性能,以提升服務器吞吐能力
  • 注意安全問題,防止/../../index.html這種相對路徑請求訪問到其餘系統文件

第一步:實現一個最簡單的靜態服務器: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 zlib = require('zlib');
  • 使用流的方式讀取文件

修改代碼以下:

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實現靜態服務器,抄抄改改哈哈【滑稽】

相關文章
相關標籤/搜索