搭建Node.js靜態伺服器

_shine發表於2018-03-17

對於初學Node.js,搭建一個靜態伺服器可以加深對TCP/IP的理解,在學習過程中參考了Node大神樸靈,本文主要記述在搭建中的思路,以加深對伺服器的瞭解。主要實現以下幾個功能:

  • 執行命令可以到指定的目錄
  • 讀取靜態檔案
  • MIME型別支援
  • 快取支援/控制
  • 支援gzip壓縮
  • 只能訪問指定目錄, 不能訪問指定目錄的上級目錄,保證安全
  • 訪問目錄可以自動尋找下面的index.html檔案
  • Range支援,斷點續傳

專案初始化

專案目錄

專案目錄

指定命令列檔案

指定命令列

建立命令列

#! /usr/bin/env node
let yargs = require('yargs');
let Server = require('../src/app.js');
let argv = yargs.option('d', {
    alias: 'root',
    demand: 'false',
    type: 'string',
    default: process.cwd(),
    description: '靜態檔案根目錄'
}).option('o', {
    alias: 'host',
    demand: 'false',
    default: 'localhost',
    type: 'string',
    description: '請配置監聽的主機'
}).option('p', {
    alias: 'port',
    demand: 'false',
    type: 'number',
    default: 9090,
    description: '請配置埠號'
})
    .usage('static-server1  [options]')
    .example(
    'static-server1 -d / -p 9890 -o localhost', '在本機的9090埠上監聽客戶端的請求'
    ).help('h').argv;

// argv = {d,root,o,host,p,port}
let server = new Server(argv);
server.start();

//static-server1
//命令列中的命令指向了npm目錄bat檔案,而 bat檔案又指向了當前目錄 的www檔案
複製程式碼

靜態伺服器核心類

  • 建立伺服器例項
    start() {
// 建立服務例項
        let server = http.createServer()
        server.on('request', this.request.bind(this))
        server.listen(this.config.port, () => {
            let url = `http://${this.config.host}:${this.config.port}`
            debug(`server started at ${chalk.green(url)}`)
        })
    }
複製程式碼
  • 建立伺服器的響應
       async request(req, res) {
          let {pathname} = url.parse(req.url)
          if (pathname === '/favicon.ico') {
              return this.sendError('not found', req, res)
          }
          let filePath = path.join(this.config.root, pathname)
          try {
              let statObj = await stat(filePath)
              if (statObj.isDirectory()) {
                  let files = await readdir(filePath)
                  files = files.map(file => ({
                      name: file,
                      url: path.join(pathname, file)
                  }))
                  let html = this.list({
                      title: pathname,
                      files
                  })
                  res.setHeader('Content-Type', 'text/html')
                  res.end(html)
              } else {
                  this.sendFile(req, res, filePath, statObj)
              }
          } catch (e) {
              debug(inspect(e)) // inspect把一個物件轉成字元
              this.sendError(e, req, res)
          }
      }
    複製程式碼
  • 對檔案型別的處理
   sendFile(req, res, filePath, statObj) {
       // 如果走快取 ,則直接返回
       if (this.handleCache(req, res, filePath, statObj)) return
       res.setHeader('Content-Type', mime.getType(filePath) + ';charset= utf-8')
       let encoding = this.getEncoding(req, res)
       let rs = this.getStream(req, res, filePath, statObj)
       if (encoding) {
           rs.pipe(encoding).pipe(res)
       } else {
           rs.pipe(res)
       }
   }
複製程式碼
  • Range支援,斷點續傳

      getStream(req, res, filePath, statObj) {
          let start = 0;
          let end = statObj.size - 1
          let range = req.headers['range']
          if (range) {
              res.setHeader('Accept-Range', 'bytes')
              res.statusCode = 206 // 返回整個內容的一塊
              let result = range.match(/bytes=(\d*)-(\d*)/)
              if (result) {
                  start = isNaN(result[1]) ? start : parseInt(result[1])
                  end = isNaN(result[2]) ? end : parseInt(result[2])
              }
          }
          return fs.createReadStream(filePath, {
              start, end
          })
      }
    
    複製程式碼
  • 處理快取

    handleCache(req, res, filePath, statObj) {
        let ifModifiedSince = req.headers['if-modified-since']
        let isNoneMatch = req.headers['is-none-match']
        res.setHeader('Cache-Control', 'private,max-age=30')
        res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toGMTString())
        let etag = statObj.size
        let lastModified = statObj.ctime.toGMTString()
        res.setHeader('ETag',etag)
        res.setHeader('Last-Modified',lastModified)
        if(isNoneMatch && isNoneMatch != etag)
        {
            return false
        }
        if(ifModifiedSince && ifModifiedSince != lastModified)
        {
            return false
        }
        if(isNoneMatch || ifModifiedSince)
        {
            res.writeHead(304)
            res.end()
            return true
        }else {
            return false
        }
    }
複製程式碼
  • 錯誤處理
  sendError(err,req,res)
    {
        res.statusCode = 500
        res.end(`${err.toString()}`)
    }

複製程式碼
  • 壓縮處理
 getEncoding(req,res)
    {
        let 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 null
        }
    }
複製程式碼

專案執行 程式碼已釋出在npm 1、 在當前目錄執行npm link

npm link
可以將當前statc-server1命令新增到命令中 具體可以檢視
image.png

image.png

可以檢視到當目錄已經生成,原來一直用的vue-cli命令就是能過這裡可以找到。 2、執行 ~set DEBUG=static*~ 3、執行命令 ~static-server1~ 預設是9090 當前可以訪問http:localhost:9090

相關文章