资讯专栏INFORMATION COLUMN

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

jzzlee / 1569人阅读

摘要:另外有哪里写的不好的地方或看不懂的地方可以给我留言。如果你觉得还有点用,给我这个上点个我会很感激你的哈个人公众号欢迎关注

静态服务器

使用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 是否自动打开浏览器

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 (/gzip/.test(acceptEncoding)) {
      res.setHeader("Content-Encoding", "gzip");
      return zlib.createGzip();
    } else if (/deflate/.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 (/gzip/.test(acceptEncoding)) {
      res.setHeader("Content-Encoding", "gzip");
      return zlib.createGzip();
    } else if (/deflate/.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而已,你也可以自己设置,这个博客写多了是在是太卡了,字都打不动了。
另外有哪里写的不好的地方或看不懂的地方可以给我留言。如果你觉得还有点用,给我github这个上点个star我会很感激你的哈

个人公众号欢迎关注

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101556.html

相关文章

  • Hexo快速搭建静态博客并实现远程VPS自动部署

    摘要:提示,如果需要通过实现服务器自动化部署,推荐使用会更方便一些然后在目录初始化本地仓库并提交到这时候上已经有我提交的代码了。再新建一个文件处理部署相关脚本,内容如下将新增的与两个文件到服务器。 这篇文章将如何搭建hexo,以及如何通过git webhooks实现远程vps的自动部署 这篇文章适合的条件: 简单的用于个人博客、公司博客展示,hexo的定位是静态博客,要实现动态服务器的功能...

    keke 评论0 收藏0
  • node.js中文资料导航

    摘要:中文资料导航官网七牛镜像深入浅出系列进阶必读中文文档被误解的编写实战系列热门模块排行榜,方便找出你想要的模块多线程,真正的非阻塞浅析的类利用编写异步多线程的实例中与的区别管道拒绝服务漏洞高级编程业界新闻看如何评价他们的首次尝鲜程序员如何说服 node.js中文资料导航 Node.js HomePage Node官网七牛镜像 Infoq深入浅出Node.js系列(进阶必读) Nod...

    geekidentity 评论0 收藏0
  • 基于 Gulp 的简易前端自动化工程搭建

    摘要:生成的文件如下由于给文件添加了哈希值,所以每次编译出来的和都是不一样的,这会导致有很多冗余文件,所以我们可以每次在生成文件之前,先将原来的文件全部清空。中也有做这个工作的插件,因此我们可以在编译压缩添加哈希值之前先将原文将清空。 原文链接:http://mrzhang123.github.io/2016/09/07/gulpUse/项目链接:https://github.com/MrZ...

    Blackjun 评论0 收藏0

发表评论

0条评论

jzzlee

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<