深入理解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入门详解(多篇文章结合)
Mar 07 NodeJs
nodejs npm包管理的配置方法及常用命令介绍
Jun 05 NodeJs
轻松创建nodejs服务器(9):实现非阻塞操作
Dec 18 NodeJs
Nodejs中读取中文文件编码问题、发送邮件和定时任务实例
Jan 01 NodeJs
基于nodejs+express(4.x+)实现文件上传功能
Nov 23 NodeJs
Nodejs全局安装和本地安装的不同之处
Jul 04 NodeJs
nodejs加密Crypto的实例代码
Jul 07 NodeJs
浅谈Nodejs应用主文件index.js
Aug 28 NodeJs
NodeJS链接MySql数据库的操作方法
Jun 27 NodeJs
nodejs 图片预览和上传的示例代码
Sep 30 NodeJs
Nodejs模块的调用操作实例分析
Dec 25 NodeJs
NodeJS 文件夹拷贝以及删除功能
Sep 03 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
PHP操作MongoDB GridFS 存储文件的详解
2013/06/20 PHP
服务器迁移php版本不同可能诱发的问题
2015/12/22 PHP
PHP抓取及分析网页的方法详解
2016/04/26 PHP
php数据库操作model类(使用__call方法)
2016/11/16 PHP
解决laravel资源加载路径设置的问题
2019/10/14 PHP
laravel框架学习笔记之组件化开发实现方法
2020/02/01 PHP
JS是否可以跨文件同时控制多个iframe页面的应用技巧
2007/12/16 Javascript
js前台分页显示后端JAVA数据响应
2013/03/18 Javascript
Jquery中使用setInterval和setTimeout的方法
2013/04/08 Javascript
dwz 如何去掉ajaxloading具体代码
2013/05/22 Javascript
cookie的复制与使用记住用户名实现代码
2013/11/04 Javascript
使用javascript控制cookie显示和隐藏背景图
2014/02/12 Javascript
JS获取数组最大值、最小值及长度的方法
2015/11/24 Javascript
javascript html5 canvas实现可拖动省份的中国地图
2016/03/11 Javascript
JavaScript实现星星等级评价功能
2017/03/22 Javascript
bootstrap treeview 扩展addNode方法动态添加子节点的方法
2017/11/21 Javascript
微信小程序 image组件遇到的问题
2019/05/28 Javascript
Python基于scapy实现修改IP发送请求的方法示例
2017/07/08 Python
Python笔记之facade模式
2019/11/20 Python
Python协程 yield与协程greenlet简单用法示例
2019/11/22 Python
将python依赖包打包成window下可执行文件bat方式
2019/12/26 Python
Django haystack实现全文搜索代码示例
2020/11/28 Python
基于python的opencv图像处理实现对斑马线的检测示例
2020/11/29 Python
python 爬取腾讯视频评论的实现步骤
2021/02/18 Python
html5通过canvas实现刮刮卡效果示例分享
2014/01/27 HTML / CSS
Joules美国官网:出色的英国风格
2017/10/30 全球购物
京东奢侈品:全球奢侈品牌
2018/03/17 全球购物
秘书岗位职责
2013/11/18 职场文书
工厂保洁员岗位职责
2013/12/04 职场文书
粗加工管理制度
2014/02/04 职场文书
会计电算化学生个人的自我评价
2014/02/08 职场文书
创先争优活动党员公开承诺书
2014/08/29 职场文书
生日赠语
2015/06/23 职场文书
2015年幼儿园国庆节活动总结
2015/07/30 职场文书
使用react+redux实现计数器功能及遇到问题
2021/06/02 Javascript
JavaScript分页组件使用方法详解
2021/07/26 Javascript