深刻nodejs-搭建靜態服務器(實現命令行)

靜態服務器

使用node搭建一個可在任何目錄下經過命令啓動的一個簡單http靜態服務器html

完整代碼連接
安裝:npm install yg-server -g
啓動:yg-servernode

可經過以上命令安裝,啓動,來看一下最終的效果git

TODO

  • 建立一個靜態服務器
  • 經過yargs來建立命令行工具
  • 處理緩存
  • 處理壓縮

初始化

  • 建立目錄:mkdir static-server
  • 進入到該目錄:cd static-server
  • 初始化項目:npm init
  • 構建文件夾目錄結構:
    圖片描述

初始化靜態服務器

  • 首先在src目錄下建立一個app.js
  • 引入全部須要的包,非node自帶的須要npm安裝一下
  • 初始化構造函數,options參數由命令行傳入,後續會講到github

    • 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;
  }
}

處理錯誤響應

在寫具體業務前,先封裝幾個處理響應的函數,分別是錯誤的響應處理,沒有找到資源的響應處理,在後面會調用這麼幾個函數來作響應npm

  • 處理錯誤
  • 返回狀態碼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來作驗證json

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而已,你也能夠本身設置,這個博客寫多了是在是太卡了,字都打不動了。
另外有哪裏寫的很差的地方或看不懂的地方能夠給我留言。若是你以爲還有點用,給我github這個上點個star我會很感激你的哈緩存

我的公衆號歡迎關注

圖片描述

相關文章
相關標籤/搜索