静态文件服务器实现
nodejs不仅仅可以用来写服务端接口,用来做静态文件服务器替代nginx的功能, 也是分分钟可以搞定的。 话不多说,先上代码:
var server=http.createServer(function (req,res){ fs.createReadStream(Path.resolve(__dirname,"."+req.url)).pipe(res); })
在项目根目录建一个hello.html文件测试一下 hello.html内容如下:
<h1>hello,world</h1>
node app.js运行,打开浏览器访问一下:
我们再回头审视一下代码,的确就只有这么简单,这要归功于node Stream类 pipe方法的强大,fs.createReadStream读取本地文件创建一个可读流(ReadStream类的实例),再使用pipe导流到res响应流,res是一个http.ServerResponse类的实例,是一个可写流,继承自 Stream类
http.ServerResponse类的继承关系如下:
安全性考虑
上述代码实现静态文件服务器后,意味着项目根目录下所有的文件(递归)都可以通过浏览器直接访问和下载了,这样会带来一些安全性的问题,想想看,你的服务器端代码和配置文件都能通过浏览器直接下载了,因此需要在代码里加一些限制,例如只能访问特定的目录下的文件和特定扩展名的文件,这样还不够,参考OWasp Top 10安全风险(第4条-不安全的对象直接引用),攻击者仍然可以通过../../目录回溯的方法访问到其它目录,对于访问路径中包含..的也要全部过滤掉。
实现mine type
mime type是指http 响应头中的content-type字段,它决定了浏览器如何解析文件,是直接当做纯文件显示(text/plain),还是做为html文件渲染(text/html),或者当做二进制文件下载,没有输出正确的mine type,可能导致图片文件无法显示,字体文件无效,视频文件无法播放的问题。要实现起来也十分简单,只需要做一个映射表,不同文件扩展名,在响应头的content-type字段中输出对应的mine type就行了。
完整代码如下:
const http=require("http"); const Path=require("path"); const fs=require("fs"); var server=http.createServer(function (req,res){ const fileName=Path.resolve(__dirname,"."+req.url); const extName=Path.extname(fileName).substr(1); if (fs.existsSync(fileName)) { //判断本地文件是否存在 var mineTypeMap={ html:'text/html;charset=utf-8', htm:'text/html;charset=utf-8', xml:"text/xml;charset=utf-8", png:"image/png", jpg:"image/jpeg", jpeg:"image/jpeg", gif:"image/gif", css:"text/css;charset=utf-8", txt:"text/plain;charset=utf-8", mp3:"audio/mpeg", mp4:"video/mp4", ico:"image/x-icon", tif:"image/tiff", svg:"image/svg+xml", zip:"application/zip", ttf:"font/ttf", woff:"font/woff", woff2:"font/woff2", } if (mineTypeMap[extName]) { res.setHeader('Content-Type', mineTypeMap[extName]); } var stream=fs.createReadStream(fileName); stream.pipe(res); } }) server.listen(80);
实现gzip
对于文本类型的文件,如html,js,css,采用gzip压缩可以大幅减少传输量,提升服务器传输性能,当然这会损耗一点服务器的cpu性能做为代价,如果客户端浏览器支持gzip压缩,则会在请求头的accept-encoding中携带gzip关键字,用node自带的zlib类就可以实现gzip压缩了,只要在stream.pip实多加一层,先导流到gzip流,再导出到res流,当然,还要在响应头中添加Content-Encoding为gzip,这样浏览器才能正确识别到http body是采用gzip算法压缩的,并进行自动解压缩。
代码如下:
const zlib = require('zlib'); if (req.headers["accept-encoding"].indexOf("gzip")>=0 && (extName=="js" || extName=="css" || extName=="html"))) { res.setHeader('Content-Encoding', "gzip"); const gzip = zlib.createGzip(); stream.pipe(gzip).pipe(res); }
客户端缓存
http协议的缓存协商流程比较长,最终在响应头中生成expire(绝对时间)和cache-control(相对时间)两个用于控制缓存过期时间的参数,浏览器下次请求该文件时,分为以下几种情况:
如果没到过期时间,浏览器不会请求文件直接读缓存
如果已到过期时间,则会在请求头中last-modified字段携带文件的最后修改日期,如果对比时间戳与服务器文件一致,则HTTP 返回 304: Not Modified
如果按下f5刷新,会在请求头中if-modified-since字段中携带缓存的过期时间,如果对比时间戳与服务器文件一致,则HTTP 返回 304: Not Modified
ctrl+f5刷新,请求头中携带 cache-control: no-cache,强制禁用缓存。重新下载文件
逻辑分支较多,但都是日期比对,搞清楚缓存协商过程比较容易写出来,有兴趣的同学可以自行实现
高性能静态文件服务器优化