node实现静态文件服务器的优势
提到这就不得不说node的优势啦,node适合的场景是高并发I/O密集型,高并发就是同一时刻访问的人特别多,大家都跟服务器说我要什么什么,在这一时刻服务器要处理的请求也特别多。那I/O密集指什么呢,先来说下什么是I/O操作,一般对文件,数据库,网络操作都算为I/0操作,与之对应的概念是CPU操作,包括解密,加密,压缩,解压等。正常一个请求过来服务器就会开启一个线程,如果同时有多个请求就会开启多个线程,但操作系统能支持的并发数量是有限的,而node作为服务器端的JavaScript,也保持了单线程的特点。单线程的好处是什么?不会阻塞后续请求的响应。(额,似乎不够具体)举个例子,假设你开了家饭店,雇了3个服务员和3个大厨,客人来了服务员接待点餐,点餐后等待大厨做好饭菜,传给服务员,服务员在拿给客人,然后服务员才能接待下一位客人,假如同时来了4位客人,那必须有一位客人等待,以上是多线程并发的情景。单线程是什么样呢,我就需要一位服务员(作为老板的你好开心,少发了很多工资),客人来了服务员接待点餐,点餐后把客人点的菜通知给大厨,大厨就开始做菜,与此同时服务员可以接待下一位客人,而不必等待大厨把菜做好,等大厨把菜做好后就会通知服务员,服务员再把菜端给客人,对应的就是非阻塞请求,就是大厨在做菜过程中(类比操作系统执行I/O操作)服务员不会失去对其它客人请求的响应(类比为服务器可以处理后续的请求,不会占用过多的CPU资源)。从2个不同模式的点菜过程就可以看出node单线程带来的好处啦。
一个静态资源服务器要实现的功能
和需求的对话
假设你作为一个node新手,来想象下你和需求方的的一段对话:
需求方:当我访问一个url时,如果是文件,返回对应文件内容,如果是目录,返回文件列表,并且文件列表是可点击的。
你:这个没问题。(内心活动:基本功能必须满足)
需求方:还有最好有缓存功能,不希望每次都请求服务器。
你:(停顿半秒)好的。(内心活动:缓存一般网站都支持,是要设置头标签啥的,具体方法要搜索啦,可以搞得定)
简单的流程
根据需求,你开始写代码之前,简单梳理了下程序的流程:
(1)在本地根据指定端口启动一个http server,等待着来自客户端的请求
(2)当请求抵达时,根据请求的url,以设置的静态文件目录为base,映射得到文件位置
(3)检查文件是否存在
(4)如果文件不存在,返回404状态码,发送not found页面到客户端
(5)如果文件存在:
* 打开文件待读取(要考虑缓存的实现)
* 设置response header
* 发送文件到客户端
(6)等待来自客户端的下一个请求
代码实现
基本功能的代码
//config.js
module.exports = {
root:process.cwd(),
hostname:"127.0.0.1",
port:"9876"
}
//dir.tpl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{title}}</title>
</head>
<body>
{{#each files}}
<a href="{{../dir}}/{{this}}">{{this}}</a>
{{/each}}
</body>
</html>
//app.js
const http = require('http');
const config = require('./config/config');
const path = require('path')
const fs =require('fs');
const Handlebars = require('handlebars')
const tplPath = path.join(__dirname,'./template/dir.tpl');
const source =fs.readFileSync(tplPath);
const template = Handlebars.compile(source.toString())
const server = http.createServer((req,res)=>{
//将客户端当前文件夹路径与请求的url拼接起来
const filePath = path.join( config.root,req.url,)
//判断目录是否存在
fs.stat(filePath,(err,stats) =>{
if(err){
res.statusCode = 404;
res.setHeader('Content-Type','text/plain');
res.end(`${filePath} is not a directory or file`);
return;
}
//如果请求路径对应是文件还是文件夹
if(stats.isFile()){
res.statusCode = 200;
res.setHeader('Content-Type','text/html');
fs.createReadStream(filePath).pipe(res);
}else if(stats.isDirectory()){
fs.readdir(filePath,(err,files)=>{
res.statusCode =200;
res.setHeader('Content-Type','text/html');
const dir =path.relative(config.root,filePath);
const data ={
title:path.basename(filePath),
dir:dir ? `/${dir}`:'',
files
}
res.end(template(data))
})
}
})
});
server.listen(config.port,config.hostname,()=>{
console.log(`Servser started at ${config.hostname}:${config.port}`)
});
到这里,就实现了静态资源服务器的基本功能,这里文件列表的渲染采用了handlebars模板引擎。我们用到handlebars模板引擎的流程是:
(1)拿到模板文件
(2使用handbars的compile()方法将模板文件编译成template
(3)将数据传给template,返回html。
这里只用到这三个方法,其它api可以去handlebars模板引擎官网看看。
我们可以看到app.js里还是有很多回调,我们可以用util模块promisify把回调的方式改成异步调用。
(1)引用对应的包
const promisify = require('util').promisify;
(2)然后将回调的方法变成异步的,举例:
const stat = promisify(fs.stat);
const readdir = promisify(fs.readdir);
(3)用await关键字调用,有一个要注意的是await必须写在async修饰的function里。
const stats = await stat(filePath)
const files = await readdir(filePath);
缓存功能的代码
先梳理下客户端发起请求,浏览器响应的流程(是否使用缓存)。
常见几个缓存头:
- Experies:返回绝对时间校验,涉及到时区等,很少用。
- Cache-Control:返回相对时间,相对上次请求的秒数,用Max-age表示
- If-Modified-Since/Last-Modified:服务器时间校验
- If-None-Match/Etag:服务器哈希校验
//cache.js
const { cache } = require('../config/config')
function refreshRes(stats, res) {
const { maxAge, expires, cacheControl, lastModified, etag } = cache;
if (expires) {
res.setHeader('Expires', (new Date(Date.now() + maxAge * 1000)).toUTCString());
}
if (cacheControl) {
res.setHeader('Cache-Control', `public,max-age=${maxAge}`);
}
if (lastModified) {
res.setHeader('Last-Modified', stats.mtime.toUTCString());
}
if (etag) {
res.setHeader('ETag', `${stats.size}-${stats.mtime.toUTCString()}`);
}
}
module.exports = function isFresh(stats, req, res) {
refreshRes(stats, res);
const lastModified = req.headers['if-modified-since'];
const etag = req.headers['if-none-match'];
if (!lastModified && !etag) {
return false;
}
if (lastModified && lastModified !== res.getHeader('Last-Modified')) {
return false;
}
if (etag && etag !== res.getHeader('ETag')) {
return false;
}
console.log("refreshRes执行完毕");
return true;
}
//判断是否过期
if(isFresh(stats,req,res)){
res.statusCode = 304;
res.end();
return;
}
总结
这个项目主要涉及到常用模块http,fs,其它的就是一些辅助的工具模块。这个静态服务器只有最基本的功能,还有一些功能,例如文件类型的判断,压缩等都没有实现,下篇文章继续更新。