respondDirectory(pathName, req, res) { const indexPagePath = path.join(pathName, this.indexPage); if (fs.existsSync(indexPagePath)) { this.respondFile(indexPagePath, req, res); } else { fs.readdir(pathName, (err, files) => { if (err) { res.writeHead(500); return res.end(err); } const requestPath = url.parse(req.url).pathname; let content = `<h1>Index of ${requestPath}</h1>`; files.forEach(file => { let itemLink = path.join(requestPath,file); const stat = fs.statSync(path.join(pathName, file)); if (stat && stat.isDirectory()) { itemLink = path.join(itemLink, 'https://www.jb51.net/'); } content += `<p><a href='https://www.jb51.net/${itemLink}'>${file}</a></p>`; }); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(content); }); } }
当需要返回目录列表时,遍历所有内容,并为每项创建一个link,作为返回文档的一部分。需要注意的是,对于子目录的href,额外添加一个尾部斜杠,这样可以避免访问子目录时的又一次重定向。
在浏览器中测试一下,输入localhost:9527/testfolder,指定的root目录下并没有名为testfolder的文件,却存在同名目录,因此第一次会收到重定向响应,并发起一个对目录的新请求。
缓存支持
为了减少数据传输,减少请求数,继续添加缓存支持。首先梳理一下缓存的处理流程:
1.如果是第一次访问,请求报文首部不会包含相关字段,服务端在发送文件前做如下处理:
如服务器支持ETag,设置ETag头
如服务器支持Last-Modified,设置Last-Modified头
设置Expires头
设置Cache-Control头(设置其max-age值)
浏览器收到响应后会存下这些标记,并在下次请求时带上与ETag对应的请求首部If-None-Match或与Last-Modified对应的请求首部If-Modified-Since。
2.如果是重复的请求:
浏览器判断缓存是否过期(通过Cache-Control和Expires确定)
如果未过期,直接使用缓存内容,也就是强缓存命中,并不会产生新的请求
如果已过期,会发起新的请求,并且请求会带上If-None-Match或If-Modified-Since,或者兼具两者
服务器收到请求,进行缓存的新鲜度再验证:
首先检查请求是否有If-None-Match首部,没有则继续下一步,有则将其值与文档的最新ETag匹配,失败则认为缓存不新鲜,成功则继续下一步
接着检查请求是否有If-Modified-Since首部,没有则保留上一步验证结果,有则将其值与文档最新修改时间比较验证,失败则认为缓存不新鲜,成功则认为缓存新鲜
当两个首部皆不存在或者验证结果是不新鲜时,发送200及最新文件,并在首部更新新鲜度。
当验证结果是缓存仍然新鲜时(也就是弱缓存命中),不需发送文件,仅发送304,并在首部更新新鲜度
为了能启用或关闭某种验证机制,我们在配置文件里增添如下配置项:
default.json:
{ ... "cacheControl": true, "expires": true, "etag": true, "lastModified": true, "maxAge": 5 }
这里为了能测试到缓存过期,将过期时间设成了非常小的5秒。
在StaticServer类中接收这些配置:
class StaticServer { constructor() { ... this.enableCacheControl = config.cacheControl; this.enableExpires = config.expires; this.enableETag = config.etag; this.enableLastModified = config.lastModified; this.maxAge = config.maxAge; }
现在,我们要在原来的respondFile前横加一杠,增加是要返回304还是200的逻辑。
respond(pathName, req, res) { fs.stat(pathName, (err, stat) => { if (err) return respondError(err, res); this.setFreshHeaders(stat, res); if (this.isFresh(req.headers, res._headers)) { this.responseNotModified(res); } else { this.responseFile(pathName, res); } }); }
准备返回文件前,根据配置,添加缓存相关的响应首部。
generateETag(stat) { const mtime = stat.mtime.getTime().toString(16); const size = stat.size.toString(16); return `W/"${size}-${mtime}"`; } setFreshHeaders(stat, res) { const lastModified = stat.mtime.toUTCString(); if (this.enableExpires) { const expireTime = (new Date(Date.now() + this.maxAge * 1000)).toUTCString(); res.setHeader('Expires', expireTime); } if (this.enableCacheControl) { res.setHeader('Cache-Control', `public, max-age=${this.maxAge}`); } if (this.enableLastModified) { res.setHeader('Last-Modified', lastModified); } if (this.enableETag) { res.setHeader('ETag', this.generateETag(stat)); } }
需要注意的是,上面使用了ETag弱验证器,并不能保证缓存文件与服务器上的文件是完全一样的。关于强验证器如何实现,可以参考etag包的源码。
下面是如何判断缓存是否仍然新鲜: