深入nodejs-搭建静态服务器(实现命令行)

栏目: Node.js · 发布时间: 5年前

内容简介:使用node搭建一个可在任何目录下通过命令启动的一个简单http静态服务器可通过以上命令安装,启动,来看一下最终的效果在写具体业务前,先封装几个处理响应的函数,分别是错误的响应处理,没有找到资源的响应处理,在后面会调用这么几个函数来做响应

静态服务器

使用node搭建一个可在任何目录下通过命令启动的一个简单http静态服务器

完整代码链接

安装: npm install yg-server -g

启动: yg-server

可通过以上命令安装,启动,来看一下最终的效果

TODO

  • 创建一个静态服务器
  • 通过yargs来创建命令行工具
  • 处理缓存
  • 处理压缩

初始化

  • 创建目录: mkdir static-server
  • 进入到该目录: cd static-server
  • 初始化项目: npm init
  • 构建文件夹目录结构:
    深入nodejs-搭建静态服务器(实现命令行)

初始化静态服务器

  • 首先在src目录下创建一个app.js
  • 引入所有需要的包,非node自带的需要npm安装一下
  • 初始化构造函数,options参数由命令行传入,后续会讲到

    • this.host 主机名
    • this.port 端口号
    • this.rootPath 根目录
    • this.cors 是否开启跨域
    • this.openbrowser 是否自动打开浏览器
const http = require('http'); // http模块
const url = require('url');   // 解析路径
const path = require('path'); // path模块
const fs = require('fs');     // 文件处理模块
const mime = require('mime'); // 解析文件类型
const crypto = require('crypto'); // 加密模块
const zlib = require('zlib');     // 压缩
const openbrowser = require('open'); //自动启动浏览器 
const handlebars = require('handlebars'); // 模版
const templates = require('./templates'); // 用来渲染的模版文件

class StaticServer {
  constructor(options) {
    this.host = options.host;
    this.port = options.port;
    this.rootPath = process.cwd();
    this.cors = options.cors;
    this.openbrowser = options.openbrowser;
  }
}

处理错误响应

在写具体业务前,先封装几个处理响应的函数,分别是错误的响应处理,没有找到资源的响应处理,在后面会调用这么几个函数来做响应

  • 处理错误
  • 返回状态码500
  • 返回错误信息
responseError(req, res, err) {
    res.writeHead(500);
    res.end(`there is something wrong in th server! please try later!`);
  }
  • 处理资源未找到的响应
  • 返回状态码404
  • 返回一个404html
responseNotFound(req, res) {
    // 这里是用handlerbar处理了一个模版并返回,这个模版只是单纯的一个写着404html
    const html = handlebars.compile(templates.notFound)();
    res.writeHead(404, {
      'Content-Type': 'text/html'
    });
    res.end(html);
  }

处理缓存

在前面的一篇文章里我介绍过node处理缓存的几种方式,这里为了方便我只使用的协商缓存,通过ETag来做验证

cacheHandler(req, res, filepath) {
    return new Promise((resolve, reject) => {
      const readStream = fs.createReadStream(filepath);
      const md5 = crypto.createHash('md5');
      const ifNoneMatch = req.headers['if-none-match'];
      readStream.on('data', data => {
        md5.update(data);
      });

      readStream.on('end', () => {
        let etag = md5.digest('hex');
        if (ifNoneMatch === etag) {
          resolve(true);
        }
        resolve(etag);
      });

      readStream.on('error', err => {
        reject(err);
      });
    });
  }

处理压缩

  • 通过请求头accept-encoding来判断浏览器支持的压缩方式
  • 设置压缩响应头,并创建对文件的压缩方式
compressHandler(req, res) {
    const acceptEncoding = req.headers['accept-encoding'];
    if (/\bgzip\b/.test(acceptEncoding)) {
      res.setHeader('Content-Encoding', 'gzip');
      return zlib.createGzip();
    } else if (/\bdeflate\b/.test(acceptEncoding)) {
      res.setHeader('Content-Encoding', 'deflate');
      return zlib.createDeflate();
    } else {
      return false;
    }
  }

启动静态服务器

  • 添加一个启动服务器的方法
  • 所有请求都交给this.requestHandler这个函数来处理
  • 监听端口号
start() {
    const server = http.createSercer((req, res) => this.requestHandler(req, res));
    server.listen(this.port, () => {
      if (this.openbrowser) {
        openbrowser(`http://${this.host}:${this.port}`);
      }
      console.log(`server started in http://${this.host}:${this.port}`);
    });
  }

请求处理

  • 通过url模块解析请求路径,获取请求资源名
  • 获取请求的文件路径
  • 通过fs模块判断文件是否存在,这里分三种情况

    • 请求路径是一个文件夹,则调用responseDirectory处理
    • 请求路径是一个文件,则调用responseFile处理
    • 如果请求的文件不存在,则调用responseNotFound处理
requestHandler(req, res) {
    // 通过url模块解析请求路径,获取请求文件
    const { pathname } = url.parse(req.url);
    // 获取请求的文件路径
    const filepath = path.join(this.rootPath, pathname);

    // 判断文件是否存在
    fs.stat(filepath, (err, stat) => {
      if (!err) {
        if (stat.isDirectory()) {
          this.responseDirectory(req, res, filepath, pathname);
        } else {
          this.responseFile(req, res, filepath, stat);
        }
      } else {
        this.responseNotFound(req, res);
      }
    });
  }

处理请求的文件

  • 每次返回文件前,先调用前面我们写的cacheHandler模块来处理缓存
  • 如果有缓存则返回304
  • 如果不存在缓存,则设置文件类型,etag,跨域响应头
  • 调用compressHandler对返回的文件进行压缩处理
  • 返回资源
responseFile(req, res, filepath, stat) {
    this.cacheHandler(req, res, filepath).then(
      data => {
        if (data === true) {
          res.writeHead(304);
          res.end();
        } else {
          res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
          res.setHeader('Etag', data);

          this.cors && res.setHeader('Access-Control-Allow-Origin', '*');

          const compress = this.compressHandler(req, res);

          if (compress) {
            fs.createReadStream(filepath)
              .pipe(compress)
              .pipe(res);
          } else {
            fs.createReadStream(filepath).pipe(res);
          }
        }
      },
      error => {
        this.responseError(req, res, error);
      }
    );
  }

处理请求的文件夹

  • 如果客户端请求的是一个文件夹,则返回的应该是该目录下的所有资源列表,而非一个具体的文件
  • 通过fs.readdir可以获取到该文件夹下面所有的文件或文件夹
  • 通过map来获取一个数组对象,是为了把该目录下的所有资源通过模版去渲染返回给客户端
responseDirectory(req, res, filepath, pathname) {
    fs.readdir(filepath, (err, files) => {
      if (!err) {
        const fileList = files.map(file => {
          const isDirectory = fs.statSync(filepath + '/' + file).isDirectory();
          return {
            filename: file,
            url: path.join(pathname, file),
            isDirectory
          };
        });
        const html = handlebars.compile(templates.fileList)({ title: pathname, fileList });
        res.setHeader('Content-Type', 'text/html');
        res.end(html);
      }
    });

app.js完整代码

const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const crypto = require('crypto');
const zlib = require('zlib');
const openbrowser = require('open');
const handlebars = require('handlebars');
const templates = require('./templates');

class StaticServer {
  constructor(options) {
    this.host = options.host;
    this.port = options.port;
    this.rootPath = process.cwd();
    this.cors = options.cors;
    this.openbrowser = options.openbrowser;
  }

  /**
   * handler request
   * @param {*} req
   * @param {*} res
   */
  requestHandler(req, res) {
    const { pathname } = url.parse(req.url);
    const filepath = path.join(this.rootPath, pathname);

    // To check if a file exists
    fs.stat(filepath, (err, stat) => {
      if (!err) {
        if (stat.isDirectory()) {
          this.responseDirectory(req, res, filepath, pathname);
        } else {
          this.responseFile(req, res, filepath, stat);
        }
      } else {
        this.responseNotFound(req, res);
      }
    });
  }

  /**
   * Reads the contents of a directory , response files list to client
   * @param {*} req
   * @param {*} res
   * @param {*} filepath
   */
  responseDirectory(req, res, filepath, pathname) {
    fs.readdir(filepath, (err, files) => {
      if (!err) {
        const fileList = files.map(file => {
          const isDirectory = fs.statSync(filepath + '/' + file).isDirectory();
          return {
            filename: file,
            url: path.join(pathname, file),
            isDirectory
          };
        });
        const html = handlebars.compile(templates.fileList)({ title: pathname, fileList });
        res.setHeader('Content-Type', 'text/html');
        res.end(html);
      }
    });
  }

  /**
   * response resource
   * @param {*} req
   * @param {*} res
   * @param {*} filepath
   */
  async responseFile(req, res, filepath, stat) {
    this.cacheHandler(req, res, filepath).then(
      data => {
        if (data === true) {
          res.writeHead(304);
          res.end();
        } else {
          res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
          res.setHeader('Etag', data);

          this.cors && res.setHeader('Access-Control-Allow-Origin', '*');

          const compress = this.compressHandler(req, res);

          if (compress) {
            fs.createReadStream(filepath)
              .pipe(compress)
              .pipe(res);
          } else {
            fs.createReadStream(filepath).pipe(res);
          }
        }
      },
      error => {
        this.responseError(req, res, error);
      }
    );
  }

  /**
   * not found request file
   * @param {*} req
   * @param {*} res
   */
  responseNotFound(req, res) {
    const html = handlebars.compile(templates.notFound)();
    res.writeHead(404, {
      'Content-Type': 'text/html'
    });
    res.end(html);
  }

  /**
   * server error
   * @param {*} req
   * @param {*} res
   * @param {*} err
   */
  responseError(req, res, err) {
    res.writeHead(500);
    res.end(`there is something wrong in th server! please try later!`);
  }

  /**
   * To check if a file have cache
   * @param {*} req
   * @param {*} res
   * @param {*} filepath
   */
  cacheHandler(req, res, filepath) {
    return new Promise((resolve, reject) => {
      const readStream = fs.createReadStream(filepath);
      const md5 = crypto.createHash('md5');
      const ifNoneMatch = req.headers['if-none-match'];
      readStream.on('data', data => {
        md5.update(data);
      });

      readStream.on('end', () => {
        let etag = md5.digest('hex');
        if (ifNoneMatch === etag) {
          resolve(true);
        }
        resolve(etag);
      });

      readStream.on('error', err => {
        reject(err);
      });
    });
  }

  /**
   * compress file
   * @param {*} req
   * @param {*} res
   */
  compressHandler(req, res) {
    const acceptEncoding = req.headers['accept-encoding'];
    if (/\bgzip\b/.test(acceptEncoding)) {
      res.setHeader('Content-Encoding', 'gzip');
      return zlib.createGzip();
    } else if (/\bdeflate\b/.test(acceptEncoding)) {
      res.setHeader('Content-Encoding', 'deflate');
      return zlib.createDeflate();
    } else {
      return false;
    }
  }

  /**
   * server start
   */
  start() {
    const server = http.createServer((req, res) => this.requestHandler(req, res));
    server.listen(this.port, () => {
      if (this.openbrowser) {
        openbrowser(`http://${this.host}:${this.port}`);
      }
      console.log(`server started in http://${this.host}:${this.port}`);
    });
  }
}

module.exports = StaticServer;

创建命令行工具

  • 首先在bin目录下创建一个config.js
  • 导出一些默认的配置
module.exports = {
  host: 'localhost',
  port: 3000,
  cors: true,
  openbrowser: true,
  index: 'index.html',
  charset: 'utf8'
};
 #! /usr/bin/env node
#! /usr/bin/env node

const yargs = require('yargs');
const path = require('path');
const config = require('./config');
const StaticServer = require('../src/app');
const pkg = require(path.join(__dirname, '..', 'package.json'));

const options = yargs
  .version(pkg.name + '@' + pkg.version)
  .usage('yg-server [options]')
  .option('p', { alias: 'port', describe: '设置服务器端口号', type: 'number', default: config.port })
  .option('o', { alias: 'openbrowser', describe: '是否打开浏览器', type: 'boolean', default: config.openbrowser })
  .option('n', { alias: 'host', describe: '设置主机名', type: 'string', default: config.host })
  .option('c', { alias: 'cors', describe: '是否允许跨域', type: 'string', default: config.cors })
  .option('v', { alias: 'version', type: 'string' })
  .example('yg-server -p 8000 -o localhost', '在根目录开启监听8000端口的静态服务器')
  .help('h').argv;

const server = new StaticServer(options);

server.start();

入口文件

最后回到根目录下的index.js,将我们的模块导出,这样可以在根目录下通过 node index 来调试

module.exports = require('./bin/static-server');

配置命令

配置命令非常简单,进入到package.json文件里

加入一句话

"bin": {
    "yg-server": "bin/static-server.js"
  },
npm link

总结

写到这里基本上就写完了,另外还有几个模版文件,是用来在客户端展示的,可以看我的 github ,我就不贴了,只是一些html而已,你也可以自己设置,这个博客写多了是在是太卡了,字都打不动了。

另外有哪里写的不好的地方或看不懂的地方可以给我留言。

个人公众号欢迎关注

深入nodejs-搭建静态服务器(实现命令行)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

可计算性和计算复杂性

可计算性和计算复杂性

朱一清 / 国防工业出版社 / 2006-4 / 18.0

本书深入浅出地介绍了研究可计算性的四个主要模型以及四个模型彼此之间的关系:介绍了计算复杂性的基本概念和重要的研究方法与一些研究成果。内容涉及递归函数、图灵机、λ演算、马尔可夫算法、计算复杂度的分类、NP完全理论、非一致复杂性等。分述于十章,书中附有习题。 本书可作为广大有志于突破计算复杂性研究僵局——“P=NP?”的科技工作者,计算机科学和元计算机科学工作者,数学和元数学工作者以及大......一起来看看 《可计算性和计算复杂性》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器