实现以下功能
- 读取静态文件或目录
- MIME类型支持
- 缓存支持/控制
- 支持gzip压缩
- 访问目录可以自动寻找下面的index.html文件
- Range支持,断点续传
- 图片防盗链
- 后台运行
基本功能读取静态文件
此功能分为两部分:
* 1、返回文件
调用fs建立可读流读取文件返回客户端
fs.createReadStream(filepath).pipe(res);
* 2、返回目录
因为是要返回当前请求路径下的所有目录,并且支持点击进入下一级,所有返回文件为html
采用handlebars模板引擎编辑模板
// 处理模板函数
let list = function () {
let template = fs.readFileSync(listTemplatePath, 'utf8');
return handlebars.compile(template);
}
// template 文件
<!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>
<ul>
{{#each files}}
<li>
<a href={{url}}>{{name}}</a>
</li>
{{/each}}
</ul>
</body>
</html>
// 读取当前路径下的所有目录
let { promisify, inspect } = util;
// 转变fs.readdir 异步方法返回promise
let readdirPromise = promisify(fs.readdir);
// 返回filepath下所有目录
let files = await readdirPromise(filepath)
// 处理files为我们想要的数据
files = files.map(file => ({
name: file,
url: path.join(pathname, file),
}))
// 最终返回客户端的html
// list
let html = this.list({
title: pathname,
files,
});
MIME类型支持
res.setHeader('Content-Type', mime.getType(filepath));
缓存支持/控制
toCache (req, res, filepath, stat) {
// 强制缓存
res.setHeader('Cache-Control', 'private,max-age=60'); // http 1.1
// private 客户端可以缓存
// public 客户端和代理服务器都可以缓存
// max-age=60 缓存内容将在60秒后失效
// no-cache 需要使用对比缓存验证数据,强制向源服务器再次验证
// no-store 所有内容都不会缓存,强制缓存和对比缓存都不会触发
res.setHeader('Expires', new Date(Date.now() + 60 * 1000).toUTCString()); // http 1.0
// 对比缓存
// last-modify
let ifModifiedSince = req.headers['if-modified-since'];
let lastModified = stat.ctime.toGMTString();
// etag
let ifNoneMatch = req.headers['if-none-match'];
let eTag = cypto.createHash('sha1').update(stat.ctime.toGMTString() + stat.size).digest('hex');
if (ifModifiedSince || ifNoneMatch) {
if (ifNoneMatch === eTag && lastModified === ifModifiedSince) {
res.statusCode = 304;
res.end('');
return false;
}
if ((ifModifiedSince && lastModified === ifModifiedSince) || (ifNoneMatch && ifNoneMatch === eTag)) {
res.statusCode = 304;
res.end('');
return false;
}
}
res.setHeader('Last-Modified', lastModified);
res.setHeader('ETag', eTag);
return true;
}
压缩支持
async gzip(req, res, gzipInfo) {
let encoding = req.headers['accept-encoding'];
let gzipReg = /\bgzip\b/;
let deflateReg = /\bdeflate\b/;
let type, streamInfo;
if (gzipReg.test(encoding)) {
streamInfo = gzipInfo ? await gzipPromise(gzipInfo) : zlib.createGzip();
type = 'gzip';
} else if (deflateReg.test(encoding)) {
streamInfo = gzipInfo ? await deflatePromise(gzipInfo) : zlib.createDeflate();
type = 'deflate';
}
if (type) {
res.setHeader('Content-Encoding', type);
}
return streamInfo;
}
访问目录可以自动寻找下面的index.html文件
let indexFilepath = path.join(filepath, '/', 'index.html');
try {
let statIndex = await statPromise(indexFilepath);
if (statIndex) {
// 返回 inde.html文件
return this.sendFile(req, res, indexFilepath, stat);
}
} catch (e) {}
Range支持,断点续传
range (req, res, filepath, stat) {
res.setHeader('Accept-Range', 'bytes'); // 通知客户端支持获取部分资源
let range = req.headers['range']; // Range: bytes=0-xxx
let start = 0, end = stat.size;
if (range) {
let result = range.match(/bytes=(\d*)-(\d*)/);
start = isNaN(result[1]) ? result[1] : start;
end = isNaN(result[2]) ? result[2] : end;
}
return fs.createReadStream(filepath, {
start,
end,
})
}
图片防盗链
notSteal (req, res) {
console.log(path.join(process.cwd(), 'imgs/load.png'))
let refer = req.headers['referer'] || req.headers['refer'];
//如果说有refer的话,则表示是从HTML页面中引用过来的
if (refer) {
let { host } = Url.parse(refer);
if (host !== hostDomain) {
// 返回默认图
return fs.createReadStream(path.join(process.cwd(), 'imgs/load.png'));
}
}
}
开启子进程运行
let { spawn } = require('child_process');
let fs = require('fs');
let serverPath = '../src/child.js';
let child = spawn('node', [serverPath], {
detached: true,
stdio: ['ignore', process.stdout, 'ignore']
})
child.unref();
- 功能尚不完善,之后会慢慢丰富!
- 更多细节源码请查看我的GitHub sevenStatic
参考
网友评论