经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Node.js » 查看文章
深入理解nodejs搭建静态服务器(实现命令行)
来源:jb51  时间:2019/2/11 9:06:15  对本文有异议

静态服务器

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

完整代码链接

安装:npm install yg-server -g

启动:yg-server

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

TODO

  • 创建一个静态服务器

  • 通过yargs来创建命令行工具

  • 处理缓存

  • 处理压缩

初始化

  • 创建目录:mkdir static-server

  • 进入到该目录:cd static-server

  • 初始化项目:npm init

  • 构建文件夹目录结构:


初始化静态服务器

  • 首先在src目录下创建一个app.js

  • 引入所有需要的包,非node自带的需要npm安装一下

  • 初始化构造函数,options参数由命令行传入,后续会讲到

    • this.host 主机名

    • this.port 端口号

    • this.rootPath 根目录

    • this.cors 是否开启跨域

    • this.openbrowser 是否自动打开浏览器

  1. const http = require('http'); // http模块
  2. const url = require('url');  // 解析路径
  3. const path = require('path'); // path模块
  4. const fs = require('fs');   // 文件处理模块
  5. const mime = require('mime'); // 解析文件类型
  6. const crypto = require('crypto'); // 加密模块
  7. const zlib = require('zlib');   // 压缩
  8. const openbrowser = require('open'); //自动启动浏览器 
  9. const handlebars = require('handlebars'); // 模版
  10. const templates = require('./templates'); // 用来渲染的模版文件
  11.  
  12. class StaticServer {
  13.  constructor(options) {
  14.   this.host = options.host;
  15.   this.port = options.port;
  16.   this.rootPath = process.cwd();
  17.   this.cors = options.cors;
  18.   this.openbrowser = options.openbrowser;
  19.  }
  20. }

处理错误响应

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

  • 处理错误

  • 返回状态码500

  • 返回错误信息

  1.  responseError(req, res, err) {
  2.   res.writeHead(500);
  3.   res.end(`there is something wrong in th server! please try later!`);
  4.  }
  • 处理资源未找到的响应

  • 返回状态码404

  • 返回一个404html

  1.  responseNotFound(req, res) {
  2.   // 这里是用handlerbar处理了一个模版并返回,这个模版只是单纯的一个写着404html
  3.   const html = handlebars.compile(templates.notFound)();
  4.   res.writeHead(404, {
  5.    'Content-Type': 'text/html'
  6.   });
  7.   res.end(html);
  8.  }

处理缓存

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

  1.  cacheHandler(req, res, filepath) {
  2.   return new Promise((resolve, reject) => {
  3.    const readStream = fs.createReadStream(filepath);
  4.    const md5 = crypto.createHash('md5');
  5.    const ifNoneMatch = req.headers['if-none-match'];
  6.    readStream.on('data', data => {
  7.     md5.update(data);
  8.    });
  9.  
  10.    readStream.on('end', () => {
  11.     let etag = md5.digest('hex');
  12.     if (ifNoneMatch === etag) {
  13.      resolve(true);
  14.     }
  15.     resolve(etag);
  16.    });
  17.  
  18.    readStream.on('error', err => {
  19.     reject(err);
  20.    });
  21.   });
  22.  }

处理压缩

  • 通过请求头accept-encoding来判断浏览器支持的压缩方式

  • 设置压缩响应头,并创建对文件的压缩方式

  1.  compressHandler(req, res) {
  2.   const acceptEncoding = req.headers['accept-encoding'];
  3.   if (/\bgzip\b/.test(acceptEncoding)) {
  4.    res.setHeader('Content-Encoding', 'gzip');
  5.    return zlib.createGzip();
  6.   } else if (/\bdeflate\b/.test(acceptEncoding)) {
  7.    res.setHeader('Content-Encoding', 'deflate');
  8.    return zlib.createDeflate();
  9.   } else {
  10.    return false;
  11.   }
  12.  }

启动静态服务器

  • 添加一个启动服务器的方法

  • 所有请求都交给this.requestHandler这个函数来处理

  • 监听端口号

  1.  start() {
  2.   const server = http.createSercer((req, res) => this.requestHandler(req, res));
  3.   server.listen(this.port, () => {
  4.    if (this.openbrowser) {
  5.     openbrowser(`http://${this.host}:${this.port}`);
  6.    }
  7.    console.log(`server started in http://${this.host}:${this.port}`);
  8.   });
  9.  }

请求处理

  • 通过url模块解析请求路径,获取请求资源名

  • 获取请求的文件路径

  • 通过fs模块判断文件是否存在,这里分三种情况

    • 请求路径是一个文件夹,则调用responseDirectory处理

    • 请求路径是一个文件,则调用responseFile处理

    • 如果请求的文件不存在,则调用responseNotFound处理

  1.  requestHandler(req, res) {
  2.   // 通过url模块解析请求路径,获取请求文件
  3.   const { pathname } = url.parse(req.url);
  4.   // 获取请求的文件路径
  5.   const filepath = path.join(this.rootPath, pathname);
  6.  
  7.   // 判断文件是否存在
  8.   fs.stat(filepath, (err, stat) => {
  9.    if (!err) {
  10.     if (stat.isDirectory()) {
  11.      this.responseDirectory(req, res, filepath, pathname);
  12.     } else {
  13.      this.responseFile(req, res, filepath, stat);
  14.     }
  15.    } else {
  16.     this.responseNotFound(req, res);
  17.    }
  18.   });
  19.  }

处理请求的文件

  • 每次返回文件前,先调用前面我们写的cacheHandler模块来处理缓存

  • 如果有缓存则返回304

  • 如果不存在缓存,则设置文件类型,etag,跨域响应头

  • 调用compressHandler对返回的文件进行压缩处理

  • 返回资源

  1.  responseFile(req, res, filepath, stat) {
  2.   this.cacheHandler(req, res, filepath).then(
  3.    data => {
  4.     if (data === true) {
  5.      res.writeHead(304);
  6.      res.end();
  7.     } else {
  8.      res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
  9.      res.setHeader('Etag', data);
  10.  
  11.      this.cors && res.setHeader('Access-Control-Allow-Origin', '*');
  12.  
  13.      const compress = this.compressHandler(req, res);
  14.  
  15.      if (compress) {
  16.       fs.createReadStream(filepath)
  17.        .pipe(compress)
  18.        .pipe(res);
  19.      } else {
  20.       fs.createReadStream(filepath).pipe(res);
  21.      }
  22.     }
  23.    },
  24.    error => {
  25.     this.responseError(req, res, error);
  26.    }
  27.   );
  28.  }

处理请求的文件夹

  • 如果客户端请求的是一个文件夹,则返回的应该是该目录下的所有资源列表,而非一个具体的文件

  • 通过fs.readdir可以获取到该文件夹下面所有的文件或文件夹

  • 通过map来获取一个数组对象,是为了把该目录下的所有资源通过模版去渲染返回给客户端

  1.  responseDirectory(req, res, filepath, pathname) {
  2.   fs.readdir(filepath, (err, files) => {
  3.    if (!err) {
  4.     const fileList = files.map(file => {
  5.      const isDirectory = fs.statSync(filepath + '/' + file).isDirectory();
  6.      return {
  7.       filename: file,
  8.       url: path.join(pathname, file),
  9.       isDirectory
  10.      };
  11.     });
  12.     const html = handlebars.compile(templates.fileList)({ title: pathname, fileList });
  13.     res.setHeader('Content-Type', 'text/html');
  14.     res.end(html);
  15.    }
  16.   });

app.js完整代码

  1. const http = require('http');
  2. const url = require('url');
  3. const path = require('path');
  4. const fs = require('fs');
  5. const mime = require('mime');
  6. const crypto = require('crypto');
  7. const zlib = require('zlib');
  8. const openbrowser = require('open');
  9. const handlebars = require('handlebars');
  10. const templates = require('./templates');
  11.  
  12. class StaticServer {
  13.  constructor(options) {
  14.   this.host = options.host;
  15.   this.port = options.port;
  16.   this.rootPath = process.cwd();
  17.   this.cors = options.cors;
  18.   this.openbrowser = options.openbrowser;
  19.  }
  20.  
  21.  /**
  22.   * handler request
  23.   * @param {*} req
  24.   * @param {*} res
  25.   */
  26.  requestHandler(req, res) {
  27.   const { pathname } = url.parse(req.url);
  28.   const filepath = path.join(this.rootPath, pathname);
  29.  
  30.   // To check if a file exists
  31.   fs.stat(filepath, (err, stat) => {
  32.    if (!err) {
  33.     if (stat.isDirectory()) {
  34.      this.responseDirectory(req, res, filepath, pathname);
  35.     } else {
  36.      this.responseFile(req, res, filepath, stat);
  37.     }
  38.    } else {
  39.     this.responseNotFound(req, res);
  40.    }
  41.   });
  42.  }
  43.  
  44.  /**
  45.   * Reads the contents of a directory , response files list to client
  46.   * @param {*} req
  47.   * @param {*} res
  48.   * @param {*} filepath
  49.   */
  50.  responseDirectory(req, res, filepath, pathname) {
  51.   fs.readdir(filepath, (err, files) => {
  52.    if (!err) {
  53.     const fileList = files.map(file => {
  54.      const isDirectory = fs.statSync(filepath + '/' + file).isDirectory();
  55.      return {
  56.       filename: file,
  57.       url: path.join(pathname, file),
  58.       isDirectory
  59.      };
  60.     });
  61.     const html = handlebars.compile(templates.fileList)({ title: pathname, fileList });
  62.     res.setHeader('Content-Type', 'text/html');
  63.     res.end(html);
  64.    }
  65.   });
  66.  }
  67.  
  68.  /**
  69.   * response resource
  70.   * @param {*} req
  71.   * @param {*} res
  72.   * @param {*} filepath
  73.   */
  74.  async responseFile(req, res, filepath, stat) {
  75.   this.cacheHandler(req, res, filepath).then(
  76.    data => {
  77.     if (data === true) {
  78.      res.writeHead(304);
  79.      res.end();
  80.     } else {
  81.      res.setHeader('Content-Type', mime.getType(filepath) + ';charset=utf-8');
  82.      res.setHeader('Etag', data);
  83.  
  84.      this.cors && res.setHeader('Access-Control-Allow-Origin', '*');
  85.  
  86.      const compress = this.compressHandler(req, res);
  87.  
  88.      if (compress) {
  89.       fs.createReadStream(filepath)
  90.        .pipe(compress)
  91.        .pipe(res);
  92.      } else {
  93.       fs.createReadStream(filepath).pipe(res);
  94.      }
  95.     }
  96.    },
  97.    error => {
  98.     this.responseError(req, res, error);
  99.    }
  100.   );
  101.  }
  102.  
  103.  /**
  104.   * not found request file
  105.   * @param {*} req
  106.   * @param {*} res
  107.   */
  108.  responseNotFound(req, res) {
  109.   const html = handlebars.compile(templates.notFound)();
  110.   res.writeHead(404, {
  111.    'Content-Type': 'text/html'
  112.   });
  113.   res.end(html);
  114.  }
  115.  
  116.  /**
  117.   * server error
  118.   * @param {*} req
  119.   * @param {*} res
  120.   * @param {*} err
  121.   */
  122.  responseError(req, res, err) {
  123.   res.writeHead(500);
  124.   res.end(`there is something wrong in th server! please try later!`);
  125.  }
  126.  
  127.  /**
  128.   * To check if a file have cache
  129.   * @param {*} req
  130.   * @param {*} res
  131.   * @param {*} filepath
  132.   */
  133.  cacheHandler(req, res, filepath) {
  134.   return new Promise((resolve, reject) => {
  135.    const readStream = fs.createReadStream(filepath);
  136.    const md5 = crypto.createHash('md5');
  137.    const ifNoneMatch = req.headers['if-none-match'];
  138.    readStream.on('data', data => {
  139.     md5.update(data);
  140.    });
  141.  
  142.    readStream.on('end', () => {
  143.     let etag = md5.digest('hex');
  144.     if (ifNoneMatch === etag) {
  145.      resolve(true);
  146.     }
  147.     resolve(etag);
  148.    });
  149.  
  150.    readStream.on('error', err => {
  151.     reject(err);
  152.    });
  153.   });
  154.  }
  155.  
  156.  /**
  157.   * compress file
  158.   * @param {*} req
  159.   * @param {*} res
  160.   */
  161.  compressHandler(req, res) {
  162.   const acceptEncoding = req.headers['accept-encoding'];
  163.   if (/\bgzip\b/.test(acceptEncoding)) {
  164.    res.setHeader('Content-Encoding', 'gzip');
  165.    return zlib.createGzip();
  166.   } else if (/\bdeflate\b/.test(acceptEncoding)) {
  167.    res.setHeader('Content-Encoding', 'deflate');
  168.    return zlib.createDeflate();
  169.   } else {
  170.    return false;
  171.   }
  172.  }
  173.  
  174.  /**
  175.   * server start
  176.   */
  177.  start() {
  178.   const server = http.createServer((req, res) => this.requestHandler(req, res));
  179.   server.listen(this.port, () => {
  180.    if (this.openbrowser) {
  181.     openbrowser(`http://${this.host}:${this.port}`);
  182.    }
  183.    console.log(`server started in http://${this.host}:${this.port}`);
  184.   });
  185.  }
  186. }
  187.  
  188. module.exports = StaticServer;

创建命令行工具

  • 首先在bin目录下创建一个config.js

  • 导出一些默认的配置

  1. module.exports = {
  2.  host: 'localhost',
  3.  port: 3000,
  4.  cors: true,
  5.  openbrowser: true,
  6.  index: 'index.html',
  7.  charset: 'utf8'
  8. };
  • 然后创建一个static-server.js

  • 这里设置的是一些可执行的命令

  • 并实例化了我们最初在app.js里写的server类,将options作为参数传入

  • 最后调用server.start()来启动我们的服务器

  • 注意 #! /usr/bin/env node这一行不能省略哦

  1. #! /usr/bin/env node
  2.  
  3. const yargs = require('yargs');
  4. const path = require('path');
  5. const config = require('./config');
  6. const StaticServer = require('../src/app');
  7. const pkg = require(path.join(__dirname, '..', 'package.json'));
  8.  
  9. const options = yargs
  10.  .version(pkg.name + '@' + pkg.version)
  11.  .usage('yg-server [options]')
  12.  .option('p', { alias: 'port', describe: '设置服务器端口号', type: 'number', default: config.port })
  13.  .option('o', { alias: 'openbrowser', describe: '是否打开浏览器', type: 'boolean', default: config.openbrowser })
  14.  .option('n', { alias: 'host', describe: '设置主机名', type: 'string', default: config.host })
  15.  .option('c', { alias: 'cors', describe: '是否允许跨域', type: 'string', default: config.cors })
  16.  .option('v', { alias: 'version', type: 'string' })
  17.  .example('yg-server -p 8000 -o localhost', '在根目录开启监听8000端口的静态服务器')
  18.  .help('h').argv;
  19.  
  20. const server = new StaticServer(options);
  21.  
  22. server.start();

入口文件

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

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

配置命令

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

加入一句话

  1.  "bin": {
  2.   "yg-server": "bin/static-server.js"
  3.  },
  • yg-server是启动该服务器的命令,可以自己定义

  • 然后执行npm link生成一个符号链接文件

  • 这样你就可以通过命令来执行自己的服务器了

  • 或者将包托管到npm上,然后全局安装,在任何目录下你都可以通过你设置的命令来开启一个静态服务器,在我们平时总会需要这样一个静态服务器

总结

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号