nodejs实现一个http-server静态文件服务器

13 篇文章 0 订阅
5 篇文章 0 订阅

nodejs实现一个http-server静态文件服务器

目录结构

  • bin 存放配置信息和启动程序
  • node_modules
  • src 存放主体资源
  • package.json
  • package-lock.json

bin > www.js 启动文件

#! /usr/bin/env node
// 指定脚本的解释程序是node,必需添加
const program = require('commander');  // 在终端上显示信息提示
const options = require('./config');
const examples = new Set();
const defaultMapping = {};
program.name('fs');
program.usage('[options]');
Object.entries(options).forEach(([key,value]) => {
  defaultMapping[key] = value;
  examples.add(value.usage);
  program.option(value.option,value.usage,value.description);
})
program.on('--help',() => {
  console.log('\r\nExamples: \r');
  examples.forEach(item => {
    console.log(`  ${item}`);
  })
})
program.parse(process.argv);

let userArgs = program.opts();
let serverOptions;
// 如果输入了端口号就对默认配置文件进行更改
if (userArgs.port > 0 && userArgs.port < 65535) {
  defaultMapping.port.default = userArgs.port;
  serverOptions = defaultMapping;
} else {
  serverOptions = defaultMapping;
}

const Server = require('../src/index');
let server = new Server(serverOptions);
server.start()

bin > config.js

const options = {
  'port': {
    option: '-p --port <n>',
    default: 8080,
    usage: 'fs --port 3000',
    description: 'set http-server port'
  },
  'gzip': {
    option: '-g --gzip <n>',
    default: 1,
    usage: 'fs --gzip 0',
    description: 'set http-server gzip'
  },
  'cache': {
    option: '-c --cache <n>',
    default: 1,
    usage: 'fs --cache 0',
    description: 'set http-server cache'
  },
  'directory': {
    option: '-d --directory <d>',
    default: process.cwd(),
    usage: 'fs --directory d:',
    description: 'set http-server work directory'
  }
}

module.exports = options;

src > utils.js

const os = require('os');
const interfaces = os.networkInterfaces();  // 获取本机的ip信息
function getIps() {  // 获取本机所有IPv4的地址
  const ips = [];
  for (let ip in interfaces) {
    interfaces[ip].find(item => {
      if (item.family === 'IPv4') {
        ips.push(item.address);
      }
    })
  }
  return ips;
}

module.exports = {
  getIps
}

src > index.js 主入口文件

const http = require('http');
const chalk = require('chalk');
const url = require('url');
const path = require('path');
const crypto = require('crypto');
const zlib = require('zlib');
const mime = require('mime');
const { createReadStream, readFileSync } = require('fs');
const fs = require('fs').promises;  // 引入promise版的fs模块
const { getIps } = require('./utils');
const ejs = require('ejs');


const template = readFileSync(path.resolve(__dirname, 'directory.html'), 'utf-8');
class Server {
  constructor(options) {  // 配置初使化
    this.port = options.port.default;
    this.cache = options.cache;
    this.gzip = options.gzip;
    this.directory = options.directory.default;
    this.template = template;
    this.handleRequest = this.handleRequest.bind(this);    // 如果handleRequest不用箭头函数则可用这种方法绑定this
  }
  // 请求处理函数
  async handleRequest(req, res) {
    const { pathname } = url.parse(req.url, true);
    // decodeURIComponent是为了对中文进行解码
    const requestFile = path.join(this.directory, decodeURIComponent(pathname));

    try {
      let fileStat = await fs.stat(requestFile);  // 获取该路径下的资源状态
      if (fileStat.isDirectory()) { // 判断是否为目录
        let dirs = await fs.readdir(requestFile); // 读取该目录下的所有文件及孢子级目录
        dirs = dirs.map((dir) => {
          return {
            name: dir,
            href: path.join(pathname, dir)
          }
        })
        dirs.push({
          name: '返回上一级目录',
          href: './'
        })
        res.setHeader('Content-Type', 'text/html;charset=utf-8');
        let fileContent = await ejs.render("" + this.template, { dirs });  // 用ejs对页面渲染并返回
        res.end(fileContent);
      } else {
        this.sendFile(req, res, requestFile, fileStat);
      }
    } catch (e) {
      this.sendError(req, res, e);
    }

  }
  // 返回文件至页面
  sendFile(req, res, filePath, fileStat) {
    if (this.cacheFile(req, res, filePath, fileStat)) { // 如果文件符合缓存策略就使用缓存,返回状态码304
      res.statusCode = 304;
      res.end();
    } else {
      res.setHeader('Content-Type', mime.getType(filePath) + ';charset=utf-8'); // 用mime模块为不同文件类型设置对应的响应头
      let createCompress; // 定义压缩流
      if (createCompress = this.gzipFile(req, res)) {  // 如果文件符合压缩策略就创建压缩流
        createReadStream(filePath).pipe(createCompress).pipe(res);  // 以流管道的形式将文件模板压缩后再输出到页面显示
      } else {
        createReadStream(filePath).pipe(res);  // 以流管道的形式将文件模板输出到页面显示
      }
    }
  }
  // 压缩方法
  gzipFile(req, res) {
    const acceptEncoding = req.headers['accept-encoding']; // 获取文件支持的压缩格式
    if (acceptEncoding) {
      if (acceptEncoding.includes('gzip')) { 
        res.setHeader('Content-Encoding', 'gzip');
        return zlib.createGzip();  // 返回gzip压缩流
      } else if (acceptEncoding.includes('deflate')) {
        res.setHeader('Content-Encoding', 'deflate');
        return zlib.createDeflate();  // 返回deflate压缩流
      }
    }
    return false; // 如果不支持就返回false
  }
  // 页面缓存,可以使用强制缓存或者协商缓存或者搭配使用,我选择搭配使用
  cacheFile(req, res, filePath, fileStat) {
    res.setHeader('Cache-Control', 'max-age=10'); // 设定强制缓存,时间为10秒
    const lastModifyed = fileStat.ctime.toGMTString(); // 获取文件最后修改时间
    const etag = crypto.createHash('md5').update(readFileSync(filePath)).digest('base64'); // 将文件内容时间摘要(通常不会对整个文件进行摘要,因为内容太多影响性能,一般选择其中一部分,由于这里只是学习使用,就不用考虑太多!)
    res.setHeader('Last-Modified', lastModifyed); // 设定最后修改时间的响应头
    res.setHeader('Etag', etag); // 设置Etag响应头
    const ifModifiedSince = req.headers['if-modified-since']; // 获取上一次修改时间
    const ifNoneMatch = req.headers['if-none-match']; // 与Etag搭配使用的请求头,对比Etag值,如果相同则表示文件没变化,可走缓存,否则重新加载
    if (ifModifiedSince !== lastModifyed) { // 判断文件的最后修改时间和请求头里最后的修改时间是否相同
      return false;
    }
    if (etag !== ifNoneMatch) { // 判断 Etag和ifNoneMatch值是否相同
      return false;
    }
    return true;
  }
  sendError(req, res, err) {
    res.end(err);
  }
  // 启动函数
  start() {
    const server = http.createServer((req, res) => {  // 如果使用传统方法定义需要注定this指向,可用bind绑定
      if (req.url === '/favicon.ico') {
        console.log('快滚!')   // 超级烦favicon.ico 终端一直出现提示,不知道怎么解决掉,不管了,能跑就行!
      }
      this.handleRequest(req, res);
    })
    // console.log(this.port);
    server.listen(this.port, () => {
      // 输出启动信息到控制台
      console.log(` Starting up http - server, serving./
 Available on:`);
      const ips = getIps();
      ips.forEach(ip => {
        console.log('   http://' + chalk.blue(ip) + ':' + chalk.green(this.port));
      })
    })
    // 错误监听
    server.on('error', err => {
      if (err.code === 'EADDRINUSE') { // 如果发现端口被战用,则将在原端口上加1再次启动服务
        server.listen(++this.port);
      }

    })
  }
}
module.exports = Server;

src > directory.html 网页列表

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- <link rel="stylesheet" href="bootstrap.css"> -->
  <title>Directory</title>
  <style>
    .list-group-flush {
      border-radius: 0;
    }

    .list-group {
      margin-left: 50px;
      display: -ms-flexbox;
      display: flex;
      -ms-flex-direction: column;
      flex-direction: column;
      padding-left: 0;
      margin-bottom: 0;
      border-radius: .25rem;
      width: 200px;
    }

    .list-group-item+.list-group-item {
      border-top-width: 0;
    }

    .list-group-item-light.list-group-item-action:focus,
    .list-group-item-light.list-group-item-action:hover {
      background-color: #ececf6;
    }

    .list-group-item-light {
      background-color: #fdfdfe;
    }

    .list-group-item {
      position: relative;
      display: block;
      padding: .75rem 1.25rem;
      background-color: #fff;
      border-bottom: 1px solid rgba(0, 0, 0, .125);
    }

    .list-group-item-action {
      width: 100%;
      color: #007bff;
      text-align: inherit;
    }

    h2 {
      margin-left: 50px;
    }
  </style>
</head>

<body>
  <h2>目录列表</h2>
  <div class="list-group list-group-flush">
    <% dirs.forEach(function(dir) { %>
      <a href="<%=dir.href%>" class="list-group-item list-group-item-action list-group-item-light">
        <%=dir.name%>
      </a>
      <% }) %>
  </div>
  <script>
    const as = document.getElementsByClassName('list-group-item');
    as[as.length - 1].style.color = '#ff6700';
  </script>
</body>

</html>
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值