实战 node 静态文件服务器

发布于 2022-11-01 22:33:57 字数 7494 浏览 180 评论 0

支持功能:

  • 读取静态文件
  • 访问目录可以自动寻找下面的 index.html 文件,如果没有 index.html 则列出文件列表
  • MIME 类型支持
  • 缓存支持/控制
  • 支持 gzip 压缩
  • Range 支持,断点续传
  • 全局命令执行
  • 子进程运行

1. 创建服务读取静态文件

首先引入 http 模块,创建一个服务器,并监听配置端口:

const http = require('http');
const server = http.createServer();

// 监听请求
server.on('request', request.bind(this));

server.listen(config.port, () => {
  console.log(`静态文件服务启动成功, 访问localhost:${config.port}`);
});

写一个 fn 专门处理请求,返回静态文件, url 模块获取路径:

const url = require('url');
const fs = require('fs');
functionrequest(req, res) {
const { pathname } = url.parse(req.url); // 访问路径const filepath = path.join(config.root, pathname); // 文件路径

fs.createReadStream(filepath).pipe(res);  // 读取文件,并响应
}

支持寻找 index.html

if (pathname === '/') {
  const rootPath = path.join(config.root, 'index.html');
  try{
    const indexStat = fs.statSync(rootPath);
    if (indexStat) {
      filepath = rootPath;
    }
  } catch(e) {
    
  }
}

访问目录时,列出文件目录:

  fs.stat(filepath, (err, stats) => {
if (err) {
  res.end('not found');
  return;
}
if (stats.isDirectory()) {
  let files = fs.readdirSync(filepath);
  files = files.map(file => ({
    name: file,
    url: path.join(pathname, file)
  }));
  let html = this.list()({
    title: pathname,
    files
  });
  res.setHeader('Content-Type', 'text/html');
  res.end(html);
}
}

html 模板:

functionlist() {
  let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'), 'utf8');
  return handlebars.compile(tmpl);
}


<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><metahttp-equiv="X-UA-Compatible"content="ie=edge"><title>{{title}}</title></head><body><h1>hope-server静态文件服务器</h1><ul>
  {{#each files}}
  <li><ahref={{url}}>{{name}}</a></li>
  {{/each}}
</ul></body></html>

2. MIME 类型支持

利用 mime 模块得到文件类型,并设置编码:

res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');

3. 缓存支持

http 协议缓存:

  • Cache-Control: http1.1 内容,告诉客户端如何缓存数据,以及规则
  • private 客户端可以缓存
  • public 客户端和代理服务器都可以缓存
  • max-age=60 缓存内容将在60秒后失效
  • no-cache 需要使用对比缓存验证数据,强制向源服务器再次验证
  • no-store 所有内容都不会缓存,强制缓存和对比缓存都不会触发
  • Expires: http1.0内容,cache-control 会覆盖,告诉客户端缓存什么时候过期
  • ETag: 内容的 hash 值 下一次客户端请求在请求头里添加 if-none-match: etag 值
  • Last-Modified: 最后的修改时间 下一次客户端请求在请求头里添加 if-modified-since: Last-Modified 值
  handleCache(req, res, stats, hash) {
 // 当资源过期时, 客户端发现上一次请求资源,服务器有发送Last-Modified, 则再次请求时带上if-modified-sinceconst ifModifiedSince = req.headers['if-modified-since'];
 // 服务器发送了etag,客户端再次请求时用If-None-Match字段来询问是否过期const ifNoneMatch = req.headers['if-none-match'];
 // http1.1内容 max-age=30 为强行缓存30秒 30秒内再次请求则用缓存  private 仅客户端缓存,代理服务器不可缓存
 res.setHeader('Cache-Control', 'private,max-age=30');
 // http1.0内容 作用与Cache-Control一致 告诉客户端什么时间,资源过期 优先级低于Cache-Control
 res.setHeader('Expires', newDate(Date.now() + 30 * 1000).toGMTString());
 // 设置ETag 根据内容生成的hash
 res.setHeader('ETag', hash);
 // 设置Last-Modified 文件最后修改时间const lastModified = stats.ctime.toGMTString();
 res.setHeader('Last-Modified', lastModified);
 
 // 判断ETag是否过期if (ifNoneMatch && ifNoneMatch != hash) {
   returnfalse;
 }
 // 判断文件最后修改时间if (ifModifiedSince && ifModifiedSince != lastModified) {
   returnfalse;
 }
 // 如果存在且相等,走缓存304if (ifNoneMatch || ifModifiedSince) {
   res.writeHead(304);
   res.end();
   returntrue;
 } else {
   returnfalse;
 }
}

4. 压缩

客户端发送内容,通过请求头里 Accept-Encoding: gzip, deflate 告诉服务器支持哪些压缩格式,服务器根据支持的压缩格式,压缩内容。如服务器不支持,则不压缩。

  getEncoding(req, res) {
  const acceptEncoding = req.headers['accept-encoding'];
  // gzip和deflate压缩if (/\bgzip\b/.test(acceptEncoding)) {
    res.setHeader('Content-Encoding', 'gzip');
    return zlib.createGzip();
  } elseif (/\bdeflate\b/.test(acceptEncoding)) {
    res.setHeader('Content-Encoding', 'deflate');
    return zlib.createDeflate();
  } else {
    returnnull;
  }
}

5. 断点续传

服务器通过请求头中的 Range: bytes=0-xxx 来判断是否是做 Range 请求,如果这个值存在而且有效,则只发回请求的那部分文件内容,响应的状态码变成 206,表示 Partial Content,并设置 Content-Range。如果无效,则返回 416 状态码,表明 Request Range Not Satisfiable。如果不包含 Range 的请求头,则继续通过常规的方式响应。

  getStream(req, res, filepath, statObj) {
  let start = 0;
  let end = statObj.size - 1;
  const range = req.headers['range'];
  if (range) {
    res.setHeader('Accept-Range', 'bytes');
    res.statusCode = 206;//返回整个内容的一块let result = range.match(/bytes=(\d*)-(\d*)/);
    if (result) {
      start = isNaN(result[1]) ? start : parseInt(result[1]);
      end = isNaN(result[2]) ? end : parseInt(result[2]) - 1;
    }
  }
  return fs.createReadStream(filepath, {
    start, end
  });
}

6. 全局命令执行

通过 npm link 实现

  • 为 npm 包目录创建软链接,将其链到 {prefix}/lib/node_modules/
  • 为可执行文件(bin)创建软链接,将其链到 {prefix}/bin/{name}

npm link 命令通过链接目录和可执行文件,实现 npm 包命令的全局可执行。

package.json 里面配置

  {
bin: {
  "hope-server": "bin/hope"
}
}

在项目下面创建 bin 目录 hope 文件,利用 yargs 配置命令行传参数

  // 告诉电脑用node运行我的文件
#! /usr/bin/env node

const yargs = require('yargs');
const init = require('../src/index.js');
const argv = yargs.option('d', {
alias: 'root',
demand: 'false',
type: 'string',
default: process.cwd(),
description: '静态文件根目录'
}).option('o', {
alias: 'host',
demand: 'false',
default: 'localhost',
type: 'string',
description: '配置监听的主机'
}).option('p', {
alias: 'port',
demand: 'false',
type: 'number',
default: 8080,
description: '配置端口号'
}).option('c', {
alias: 'child',
demand: 'false',
type: 'boolean',
default: false,
description: '是否子进程运行'
})
.usage('hope-server [options]')
.example(
'hope-server -d / -p 9090 -o localhost', '在本机的9090端口上监听客户端的请求'
).help('h').argv;

// 启动服务
init(argv);

7. 子进程运行

通过 spawn 实现

index.js

const { spawn } = require('child_process');
const Server = require('./hope');

functioninit(argv) {
  // 如果配置为子进程开启服务if (argv.child) {
    //子进程启动服务const child = spawn('node', ['hope.js', JSON.stringify(argv)], {
      cwd: __dirname,
      detached: true,
      stdio: 'inherit'
    });

    //后台运行
    child.unref();
    //退出主线程,让子线程单独运行
    process.exit(0);
  } else {
    const server = new Server(argv);
    server.start();
  }
}

module.exports = init;

hope.js

if (process.argv[2] && process.argv[2].startsWith('{')) {
const argv = JSON.parse(process.argv[2]);
const server = new Hope(argv);
server.start();
}

8. 源码及测试

源码地址:hope-server

npm install hope-server -g

进入任意目录

hope-server

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

对你而言

暂无简介

文章
评论
30 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文