node之搭建一個http完整的靜態伺服器(命令列工具)

蘆夢宇發表於2018-06-17

本文主要寫一個完整的靜態服務(命令列工具),http壓縮、快取以及範圍請求。 大致功能是:在當前目錄啟動一個服務,服務用來監聽請求,處理請求。並在瀏覽器上渲染出當前服務下的所有目錄
1.配置一個服務引數物件

let path = require('path');
let config = {
  hostname:'localhost',
  port:3000,
  dir:path.join(__dirname,'..','public')
}

module.exports = config;
複製程式碼

2.寫出服務的基本框架

let config = require('./config');
let template = fs.readFileSync(path.resolve(__dirname, 'tmpl.html'), 'utf8');
class Server {
  constructor(args) {
    this.config = config;// 將配置掛載在我們的例項上
    this.template = template; //用於渲染當前服務下的所有目錄
  }
  //處理請求
  handleRequest() {
      
  }
 start() {
    let server = http.createServer(this.handleRequest.bind(this));
    let { hostname, port } = this.config;
    server.listen(port, hostname);
  }
}
複製程式碼

這樣一個服務就可以開啟了,下面寫如何處理請求。 處理請求主要功能是:接收請求後,解析當前請求路徑下,判斷該路徑是否是目錄,如果是,則解析該目錄下所有子目錄,渲染到頁面上,如果不是,則直接渲染該檔案。為了方便處理,我們使用async+await實現路徑解析,程式碼如下:

 async handleRequest(req, res) { 
    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) {
      // 檔案不存在的情況
      //錯誤
    }
  }
複製程式碼

其中用到的模版功能是將當前目錄下的子檔案,渲染到頁面上,程式碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title><%=title%></title>
</head>
<body>
  <%dirs.map(item=>{%>
    <li><a href="<%=item.pathname%>"><%=item.filename%></a></li>  
  <%})%>
</body>
</html>
複製程式碼

這樣一個基本的http服務就成形了,結下來我們實現sendFile功能,主要包括:快取、壓縮、範圍請求。 快取:主要包括強制快取和對比快取。程式碼如下:

 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) { // if-modified-since和last-modified
      return false;
    }
    if (match != smatch) {
      return false
    }
    return true;
  }
複製程式碼

實現範圍請求:主要根據 請求頭Range,如Range:bytes=0-3表示請求前四個位元組,將響應發給瀏覽器時,需要設定響應頭 Accept-Ranges: bytes
Content-Length: 4
Content-Range: bytes 0-3/7877
設定狀態碼206

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}
    }
  }
複製程式碼

檔案壓縮功能:判斷請求頭中的Accept-Encoding是否有gzip,deflate等,有就壓縮,並設定響應頭Content-Encoding

// 實現服務端壓縮
  gzip(req, res, p, stat) {
    let header = req.headers['accept-encoding'];
    if (header) {
      if (header.match(/\bgzip\b/)) {
        res.setHeader('Content-Encoding', 'gzip');
        fs.createReadStream(p).pipe(zlib.createGzip()).pipe(res);
      } else if (header.match(/\bdeflate\b/)) {
        res.setHeader('Content-Encoding', 'deflate');
        fs.createReadStream(p).pipe(zlib.createDeflate()).pipe(res);
      }
    } else {
      fs.createReadStream(p).pipe(res);
    }
  }
複製程式碼

然後綜合快取,壓縮,範圍請求,整理下sendFile方法

sendFile(req, res, p, stat) {
    if (this.cache(req, res, p, stat)) {// 檢測是否有快取
      res.statusCode = 304;
      res.end();
      return
    };

    this.compressAndRange(req, res, p, stat);
  }
  compressAndRange(req, res, p, stat) {
      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);
      }
  }
複製程式碼

這樣伺服器的功能都具備了,在實現一個命令列工具 在package.json中,新增:

 "bin": {
    "my-http-server": "bin/www.js"
  }
複製程式碼

然後npm link
在bin/www.js 中啟動http server,這樣就可以通過my-http-server 命令啟動http伺服器. 寫www.js

#! /usr/bin/env node
let Server = require('../src/app');
new Server().start(); // 開啟服務
複製程式碼

這樣就可以通過my-http-server啟動服務,但是如果我想在任意目錄任意埠,任意域名下訪問呢。 使用yargs包,通過命令列引數控制

#! /usr/bin/env node
// 第一執行了命令後 會執行 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('zf-http-server [options]').argv;
let Server = require('../src/app');
new Server(argv).start(); // 開啟服務
複製程式碼

注意這裡將引數argv傳入了Server類,然後需要修改下Server

class Server {
  constructor(args) {
    this.config = { ...config, ...args };// 將命令列中的引數傳給server
    this.template = template;
  }
  ...
  ...
}
複製程式碼

這樣我們通過:my-http-server -host localhost -p 3003 -d './public'命令就可以指定埠(3003),目錄(當前命令所在目錄/public),域名(localhost)啟動服務

相關文章