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


Posted in NodeJs onFebruary 05, 2019

静态服务器

使用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'
};
  • 然后创建一个static-server.js
  • 这里设置的是一些可执行的命令
  • 并实例化了我们最初在app.js里写的server类,将options作为参数传入
  • 最后调用server.start()来启动我们的服务器
  • 注意 #! /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"
 },
  • yg-server是启动该服务器的命令,可以自己定义
  • 然后执行npm link生成一个符号链接文件
  • 这样你就可以通过命令来执行自己的服务器了
  • 或者将包托管到npm上,然后全局安装,在任何目录下你都可以通过你设置的命令来开启一个静态服务器,在我们平时总会需要这样一个静态服务器

总结

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

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

NodeJs 相关文章推荐
nodejs教程 安装express及配置app.js文件的详细步骤
May 11 NodeJs
轻松创建nodejs服务器(6):作出响应
Dec 18 NodeJs
简单好用的nodejs 爬虫框架分享
Mar 26 NodeJs
nodejs入门教程三:调用内部和外部方法示例
Apr 24 NodeJs
详解Nodejs之静态资源处理
Jun 05 NodeJs
在Debian(Raspberry Pi)树莓派上安装NodeJS的教程详解
Sep 19 NodeJs
nodejs实现OAuth2.0授权服务认证
Dec 27 NodeJs
nodejs简单读写excel内容的方法示例
Mar 16 NodeJs
详解webpack打包nodejs项目(前端代码)
Sep 19 NodeJs
详解nodejs http请求相关总结
Mar 31 NodeJs
关于NodeJS中的循环引用详解
Jul 23 NodeJs
Nodejs实现的操作MongoDB数据库功能完整示例
Feb 02 #NodeJs
基于Koa(nodejs框架)对json文件进行增删改查的示例代码
Feb 02 #NodeJs
用Electron写个带界面的nodejs爬虫的实现方法
Jan 29 #NodeJs
NVM安装nodejs的方法实用步骤
Jan 16 #NodeJs
nodeJS进程管理器pm2的使用
Jan 09 #NodeJs
NodeJS模块与ES6模块系统语法及注意点详解
Jan 04 #NodeJs
nodejs 使用http进行post或get请求的实例(携带cookie)
Jan 03 #NodeJs
You might like
封装一个PDO数据库操作类代码
2009/09/09 PHP
php用数组返回无限分类的列表数据的代码
2010/08/08 PHP
PHP对象Object的概念 介绍
2012/06/14 PHP
PHP中file_exists()判断中文文件名无效的解决方法
2014/11/12 PHP
php上传文件并显示上传进度的方法
2015/03/24 PHP
JavaScript Eval 函数使用
2010/03/23 Javascript
jQuery实现的向下图文信息滚动效果
2015/05/03 Javascript
Node.js 实现简单小说爬虫实例
2016/11/18 Javascript
JQueryEasyUI之DataGrid数据显示
2016/11/23 Javascript
利用Node.js对文件进行重命名
2017/03/12 Javascript
bootstrapvalidator之API学习教程
2017/06/29 Javascript
layer子层给父层页面元素赋值,以达到向父层页面传值的效果实例
2017/09/22 Javascript
解决在vue项目中,发版之后,背景图片报错,路径不对的问题
2018/03/06 Javascript
vuex 的简单使用
2018/03/22 Javascript
Python和C/C++交互的几种方法总结
2017/05/11 Python
Python实现利用最大公约数求三个正整数的最小公倍数示例
2017/09/30 Python
python3写的简单本地文件上传服务器实例
2018/06/04 Python
Django实现登录随机验证码的示例代码
2018/06/20 Python
python实现反转部分单向链表
2018/09/27 Python
python 获取图片分辨率的方法
2019/01/08 Python
对Python 检查文件名是否规范的实例详解
2019/06/10 Python
scrapy爬虫:scrapy.FormRequest中formdata参数详解
2020/04/30 Python
Python实例方法、类方法、静态方法区别详解
2020/09/05 Python
Python爬虫爬取有道实现翻译功能
2020/11/27 Python
Python制作运行进度条的实现效果(代码运行不无聊)
2021/02/24 Python
利用html5 file api读取本地文件示例(如图片、PDF等)
2018/03/07 HTML / CSS
Internal修饰符有什么含义
2013/07/10 面试题
拾金不昧表扬信范文
2014/01/11 职场文书
浪漫婚礼主持词
2014/03/14 职场文书
高校教师岗位职责
2014/03/18 职场文书
银行青年文明号事迹材料
2014/05/31 职场文书
缓刑人员思想汇报500字
2014/09/12 职场文书
广告设计专业毕业生自我鉴定
2014/09/27 职场文书
2014年心理健康教育工作总结
2014/12/06 职场文书
2015年社区环境卫生工作总结
2015/04/21 职场文书
Java实现简易的分词器功能
2021/06/15 Java/Android