使用Node.js搭建静态资源服务详细教程(2)

读取文件,这里用的是流的形式createReadStream而不是readFile,是因为后者会在得到完整文件内容之前将其先读到内存里。这样万一文件很大,再遇上多个请求同时访问,readFile就承受不来了。使用文件可读流,服务端不用等到数据完全加载到内存再发回给客户端,而是一边读一边发送分块响应。这时响应里会包含如下响应头:

Transfer-Encoding:chunked

默认情况下,可读流结束时,可写流的end()方法会被调用。

MIME支持

现在给客户端返回文件时,我们并没有指定Content-Type头,虽然你可能发现访问文本或图片浏览器都可以正确显示出文字或图片,但这并不符合规范。任何包含实体主体(entity body)的响应都应在头部指明文件类型,否则浏览器无从得知类型时,就会自行猜测(从文件内容以及url中寻找可能的扩展名)。响应如指定了错误的类型也会导致内容的错乱显示,如明明返回的是一张jpeg图片,却错误指定了header:'Content-Type': 'text/html',会收到一堆乱码。

使用Node.js搭建静态资源服务详细教程

虽然有现成的mime模块可用,这里还是自己来实现吧,试图对这个过程有更清晰的理解。

在根目录下创建mime.js文件:

const path = require('path'); const mimeTypes = { "css": "text/css", "gif": "image/gif", "html": "text/html", "ico": "image/x-icon", "jpeg": "image/jpeg", ... }; const lookup = (pathName) => { let ext = path.extname(pathName); ext = ext.split('.').pop(); return mimeTypes[ext] || mimeTypes['txt']; } module.exports = { lookup };

该模块暴露出一个lookup方法,可以根据路径名返回正确的类型,类型以‘type/subtype'表示。对于未知的类型,按普通文本处理。

接着在static-server.js中引入上面的mime模块,给返回文件的响应都加上正确的头部字段:

respondFile(pathName, req, res) { const readStream = fs.createReadStream(pathName); res.setHeader('Content-Type', mime.lookup(pathName)); readStream.pipe(res); }

重新运行程序,会看到图片可以在浏览器中正常显示了。

使用Node.js搭建静态资源服务详细教程

Note:

需要注意的是,Content-Type说明的应是原始实体主体的文件类型。即使实体经过内容编码(如gzip,后面会提到),该字段说明的仍应是编码前的实体主体的类型。

添加其他功能

至此,已经完成了基本功能中列出的几个步骤,但依然有很多需要改进的地方,比如如果用户输入的url对应的是磁盘上的一个目录怎么办?还有,现在对于同一个文件(从未更改过)的多次请求,服务端都是勤勤恳恳地一遍遍地发送回同样的文件,这些冗余的数据传输,既消耗了带宽,也给服务器添加了负担。另外,服务器如果在发送内容之前能对其进行压缩,也有助于减少传输时间。

读取文件目录

现阶段,用url: localhost:9527/testfolder去访问一个指定root文件夹下真实存在的testfolder的文件夹,服务端会报错:

Error: EISDIR: illegal operation on a directory, read

要增添对目录访问的支持,我们重新整理下响应的步骤:

1.请求抵达时,首先判断url是否有尾部斜杠

2.如果有尾部斜杠,认为用户请求的是目录

如果目录存在

如果目录下存在默认页(如index.html),发送默认页

如果不存在默认页,发送目录下内容列表

如果目录不存在,返回404

3.如果没有尾部斜杠,认为用户请求的是文件

如果文件存在,发送文件

如果文件不存在,判断同名的目录是否存在

如果存在该目录,返回301,并在原url上添加上/作为要转到的location

如果不存在该目录,返回404

我们需要重写一下routeHandler内的逻辑:

routeHandler(pathName, req, res) { fs.stat(pathName, (err, stat) => { if (!err) { const requestedPath = url.parse(req.url).pathname; if (hasTrailingSlash(requestedPath) && stat.isDirectory()) { this.respondDirectory(pathName, req, res); } else if (stat.isDirectory()) { this.respondRedirect(req, res); } else { this.respondFile(pathName, req, res); } } else { this.respondNotFound(req, res); } }); }

继续补充respondRedirect方法:

respondRedirect(req, res) { const location = req.url + 'https://www.jb51.net/'; res.writeHead(301, { 'Location': location, 'Content-Type': 'text/html' }); res.end(`Redirecting to <a href='https://www.jb51.net/${location}'>${location}</a>`); }

浏览器收到301响应时,会根据头部指定的location字段值,向服务器发出一个新的请求。

继续补充respondDirectory方法:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wydpsj.html