用node搭一个静态服务

261 阅读4分钟

如何搭一个静态服务

  1. 新建一个文件夹
  2. 初始化 npm init -y
  3. 所用到的模块
  • http-server 起服务
  • mime chalk debug ejs 所需模块
  • http fs util path自带模块
  • yargs

目录结构

启动一个服务需要 src/congfig文件

  • 运行的条件 指定主机名
  • 指定启动的端口号
  • 自定运行的目录

通过config配置 app起服务 tepl编译 bin和package.json链接命令行

配置

config.js

//将配置挂载在我们的实例上,方便后期手动指定,直接get就可以
let path = require('path')
let config = {
    hostname : '127.0.0.1', //主机
    port:3000, //端口号
    dir:path.join(__dirname, 'public')
}
module.exports = config;

tmpl

<!--ejs模版,当是文件夹的时候会用到-->
<body>
    <%dirs.map(item=>{%>
        <li><a href="<%=item.pathname%>"><%=item.filename%></a></li>  
    <%})%>
</body>

配置全局管理 (全局映射关系)

package.json

//添加bin配置
"bin": {
"zdl": "bin/www.js"
},

执行npm link

bin/www.js 用此文件作为入口,帮我们引用app.js

#! /usr/bin/env node
//yargs用法 在命令行输入`zdl --help`测试是否生效
// 第一执行了命令后 会执行 bin/www.js
let yargs = require('yargs')
let argv = yargs.option('port', {
  alias: 'p',
  default: 3000, //默认
  demand: false, //是否必填
  description: 'this is port'
}).option('hostname', {
  alias: 'host',
  default: 'localhost',
  type: String,  //类型
  demand: false,
  description: 'this is hostname'
}).option('dir', {
  alias: 'd',
  default: process.cwd(),
  type: String,
  demand: false,
  description: 'this is cwd'
}).usage('zdl [options]').argv; //用法提示
//用此文件作为入口,帮我们引用app.js
let Server = require('../src/app');
new Server(argv).start(); // 开启服务
//相当于在app.js执行
//let server = new Server(argv);
//server.start();

app.js

架子

// 实现一个静态服务
let http = require('http');
let fs = require('fs');
let url = require('url');
let util = require('util');
let path = require('path');

let mime = require('mime');
let ejs = require('ejs'); // 渲染模板
let chalk = require('chalk'); // 粉笔
let debug = require('debug')('*');//所有的都输出
// 第二个参数可以指定环境变量在为什么值时才打印
// window set DEBUG=XXX  export DEBUG=XXXX


class Server {
    constructor(args) { //config在调用此文件时传参args
        constructor(args) {
        this.config = {...config,...args},
        this.template = template;//渲染模版
    }
    async handleRequest(req, res) { // 这里的this都是实例
        let { pathname } = url.parse(req.url, true);
        let p = path.join(this.config.dir, pathname);
        try{
            let statObj = await stat(p);
            if (statObj.isDirectory()) {//若是文件夹直接渲染数据
                let dirs = await readdir(p);
                dirs = dirs.map(dir => { 
                    return {
                        filename: dir, 
                        pathname: path.join(pathname, dir)
                    }
                });  
                let str = ejs.render(this.template, { dirs, title: 'ejs' });//template渲染模板,数据,渲染结果是字符串
                res.setHeader('Content-Type', 'text/html;charset=utf8');
                res.end(str);
            } else {// 文件 发送文件
                this.sendFile(req, res, p, statObj);
            }
        } catch (e) {// 文件不存在的情况
            this.sendError(req, res, e);
        }
    }
    //创建服务
    start() {
        let server = http.createServer(this.handleRequest.bind(this));//this.handleRequest,this是回调函数的this,也可以在该函数return 箭头函数
        let { hostname, port } = this.config;
        debug(`http://${hostname}:${chalk.green(port)} start`)
        server.listen(port, hostname);
    }
    //错误处理
    sendError(){
        // 解析字符串打印对象
        //debug(util.inspect(e).toString());
        res.statusCode = 404;
        res.end(`Not Found`);
    }
    //发送文件
    sendFile(){
        ...
    }
}

module.exports = Server;

添加功能

添加的三个功能都是http的应用,想更具体请参考node~http缓存

class Server {
    // 添加缓存功能呢
    cache(req, res, p, stat) {
    }
    //添加压缩功能
    gzip(req, res, p, stat) {
    }
    //添加范围请求功能
    range(req, res, p, stat) {
    }
    //如果是文件
    sendFile(req, res, p, stat) {
    }
}

如果是目录 就将目录中的内容展现出来. 如果是文件就将文件展示出来

cache

    // 添加缓存功能呢
    cache(req, res, p, stat) { //Catch-Control Expries  if-modified-since if-none-match
        let since = req.headers['if-modified-since'];
        let match = req.headers['if-none-match'];
        let ssince = stat.ctime.toUTCString();
        let smatch = stat.ctime.getTime() + stat.size;
        res.setHeader('Last-Modified', ssince);
        res.setHeader('ETag', smatch);
        res.setHeader('Cache-Control', 'max-age=6');
        if (since != ssince) {
          debug(since, ssince);
          return false;
        }
        if (match != smatch) {
          debug(match, smatch);
          return false
        }
        return true;
    }
    //如果是文件
    sendFile(req, res, p, stat) {
        if (this.cache(req, res, p, stat)) {// 检测是否有缓存
              res.statusCode = 304;
              res.end();
              return;
        }
        res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
        fs.createReadStream(p, { start, end }).pipe(res);
    }

gzip

    //添加压缩功能
    gzip(req, res, p, stat) {
        let header = req.headers['accept-encoding'];
        if (header) {
            if (header.match(/\bgzip\b/)) {
                res.setHeader('Content-Encoding', 'gzip')
                return zlib.createGzip();
            } else if (header.match(/\bdeflate\b/)) {
                res.setHeader('Content-Encoding', 'deflate')
                return zlib.createDeflate();
            }
        } else {
          return false;
        }
    }
    //如果是文件
    sendFile(req, res, p, stat) {
        ...
        let compress = this.gzip(req, res, p, stat);
        if (compress) { // 返回的是一个压缩流
            res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
            fs.createReadStream(p).pipe(compress).pipe(res);
        } else {
            res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
            fs.createReadStream(p).pipe(res);
        }
    }

range

    //添加范围请求功能
    range(req, res, p, stat) {
        let range = req.headers['range'];
        if(range){
          let [, start, end] = range.match(/(\d*)-(\d*)/) || [];
          start = start ? parseInt(start) : 0;
          end = end ? parseInt(end) : stat.size;
          res.statusCode = 206;
          res.setHeader('Accept-Ranges', 'bytes');
          res.setHeader('Content-Length', end - start + 1);
          res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
          return { start, end };
        }else{
          return {start:0,end:stat.size}
        }
    }
    //如果是文件
    
    sendFile(req, res, p, stat) {
        let { start, end } = this.range(req, res, p, stat);
        if (compress) { // 返回的是一个压缩流
        res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
            fs.createReadStream(p, { start, end }).pipe(compress).pipe(res);
        } else {
            res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
          fs.createReadStream(p, { start, end }).pipe(res);
        }
    }

小结

以下是整个app.js的内容

// 实现一个静态服务
// 如果是目录 就将目录中的内容展现出来
// 如果是文件就将文件展示出来
let http = require('http');
let fs = require('fs');
let url = require('url');
let zlib = require('zlib');
let util = require('util');
let path = require('path');

let mime = require('mime');
let ejs = require('ejs'); // 渲染模板
let chalk = require('chalk'); // 粉笔
// 第二个参数可以指定环境变量在为什么值时才打印
// window set DEBUG=XXX  export DEBUG=XXXX
let debug = require('debug')('*');
// 运行的条件 指定主机名
// 指定启动的端口号
// 自定运行的目录
let stat = util.promisify(fs.stat);
let readdir = util.promisify(fs.readdir);
let config = require('./config');
let template = fs.readFileSync(path.resolve(__dirname, 'tmpl.html'), 'utf8');
class Server {
  constructor(args) {
    this.config = { ...config, ...args };// 将配置挂载在我们的实例上
    this.template = template;
  }
  async handleRequest(req, res) { // 这里的this都是实例
    let { pathname } = url.parse(req.url, true);
    let p = path.join(this.config.dir, pathname);
    // 1.根据路径 如果是文件夹 显示文件夹里的内容
    // 2.如果是文件 显示文件的内容
    try { // 如果没错误说明文件存在
      let statObj = await stat(p);
      if (statObj.isDirectory()) {
        // 现在需要一个当前目录下的解析出的对象或者数组
        let dirs = await readdir(p);
        dirs = dirs.map(dir => { // dirs就是要渲染的数据
          return {
            filename: dir,
            pathname: path.join(pathname, dir)
          }
        });
        let str = ejs.render(this.template, { dirs, title: 'ejs' });
        res.setHeader('Content-Type', 'text/html;charset=utf8');
        res.end(str);
      } else {
        // 文件 发送文件
        this.sendFile(req, res, p, statObj);
      }
    } catch (e) {
      // 文件不存在的情况
      this.sendError(req, res, e);
    }
  }
  // 实现其他功能
  // 实现范围请求

  // 实现缓存
  // 服务器  Cache-Control Expires  
  // Last-Modified  ETag:ctime+size
  // 客户端
  // if-modified-since if-none-match
  cache(req, res, p, stat) {
    // 实现缓存 
    let since = req.headers['if-modified-since'];
    let match = req.headers['if-none-match'];
    let ssince = stat.ctime.toUTCString();
    let smatch = stat.ctime.getTime() + stat.size;
    res.setHeader('Last-Modified', ssince);
    res.setHeader('ETag', smatch);
    res.setHeader('Cache-Control', 'max-age=6');
    if (since != ssince) {
      debug(since, ssince);
      return false;
    }
    if (match != smatch) {
      debug(match, smatch);
      return false
    }
    return true;
  }
  // 实现服务端压缩
  gzip(req, res, p, stat) {
    let header = req.headers['accept-encoding'];
    if (header) {
      if (header.match(/\bgzip\b/)) {
        res.setHeader('Content-Encoding', 'gzip')
        return zlib.createGzip();
      } else if (header.match(/\bdeflate\b/)) {
        res.setHeader('Content-Encoding', 'deflate')
        return zlib.createDeflate();
      }
    } else {
      return false;
    }
  }
  range(req, res, p, stat) {
    let range = req.headers['range'];
    if(range){
      let [, start, end] = range.match(/(\d*)-(\d*)/) || [];
      start = start ? parseInt(start) : 0;
      end = end ? parseInt(end) : stat.size;
      res.statusCode = 206;
      res.setHeader('Accept-Ranges', 'bytes');
      res.setHeader('Content-Length', end - start + 1);
      res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
      return { start, end };
    }else{
      return {start:0,end:stat.size}
    }
  }
  sendFile(req, res, p, stat) {
    if (this.cache(req, res, p, stat)) {// 检测是否有缓存
      res.statusCode = 304;
      res.end();
      return
    };
    let compress = this.gzip(req, res, p, stat);
    let { start, end } = this.range(req, res, p, stat); //范围请求,返回开始位置和结束位置
    if (compress) { // 返回的是一个压缩流
      res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
      fs.createReadStream(p, { start, end }).pipe(compress).pipe(res);
    } else {
      res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
      fs.createReadStream(p, { start, end }).pipe(res);
    }
  }
  sendError(req, res, e) {
    // 解析字符串打印对象
    //debug(util.inspect(e).toString());
    res.statusCode = 404;
    res.end(`Not Found`);
  }
  start() {
    let server = http.createServer(this.handleRequest.bind(this));
    let { hostname, port } = this.config;
    debug(`http://${hostname}:${chalk.green(port)} start`)
    server.listen(port, hostname);
  }
}
// 开启一个服务
module.exports = Server  

现在你只需要在cmd里面输入zdl就可以启动一个静态服务了

发布到npm

发包

在用的时候我们可以直接 npm install [name] 然后zdl运行