用node模擬一個簡單的靜態服務(女神鎮樓)

女神鎮樓css

咱們都知道在本地起個服務直接一個http-server -p 3000 一個端口爲3000的服務就起來了,咱們能夠直接在瀏覽器訪問3000端口,就能拿到咱們須要的頁面,那麼若是想本身實現一個這樣的工具怎麼作呢?不要急,看我慢慢分析寫出來html

寫以前咱們先要搞清楚要作什麼用本身寫的包,起一個服務,訪問3000端口回車,應該顯示出public下的目錄列表,後面加/index.html,就應該顯示index.html的內容來node

  • 首先先init一個項目,並下載一些包,mime(解析返回頭類型),chalk(五光十色的輸出),debug
    這裏寫圖片描述
  • 創建本身的目錄結構:

這裏寫圖片描述
index.css

body{
    background: red
}
複製代碼

index.htmlios

<!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>Document</title>
</head>
<body>
    我很美
    <link rel="stylesheet" href="/index.css">
</body>
</html>
複製代碼

config.jsgit

let path = require('path')
//啓動服務的配置項
let config = {
    hostname:'localhost',
    port:3000,
    dir:path.join(__dirname,'..','public')
}
module.exports = config
複製代碼

app.js 這裏用到了debug(用法請看這裏)github

// set DEBUG=static:app (win32 // export DEBUG=static:app (iosweb

let config = require('./config')
let path = require('path')
let fs = require('fs')
let mime = require('mime')
let chalk = require('chalk')
let util = require('util')
let url = require('url')
let http = require('http')
let stat = util.promisify(fs.stat)
//debug 能夠後面放參數,能夠根據後面的參數決定是否打印

let debug = require('debug')('static:app') 
//console.log(chalk.green('hello'));
//debug('app')

class Server {  //首先寫一個Server類
    constructor(){
        this.config = config
    }
    handleRequest(){
        return (req,res)=>{
        }
    }
    start(){   //實例上的start方法
        let {port,hostname} = this.config
       let server =  http.createServer(this.handleRequest())
       //用http啓動一個服務,回調裏執行handleRequest方法
        let url = `http://${hostname}:${chalk.green(port)}`
         debug(url);
        server.listen(port, hostname);
    }
}
let server = new Server()
server.start()
複製代碼

node執行app.js,(在執行以前要先執行set DEBUG=static:app)獲得下圖 chrome

這裏寫圖片描述
若是你想實時監控項目的變化能夠安裝一個supervisor(npm install supervisor -g),直接執行supervisor app.js就能監控了,不過不是很穩定······ 這個時候能夠假設訪問的是http://localhost:3000/index.html,是個文件,咱們就能夠寫handleRequest方法了

handleRequest(){
        return async(req,res)=>{
            //處理路徑
            let {pathname} = url.parse(req.url,true)
            //由於拿到的pathname會是/index,這樣會直接指向c盤,加./的話就變成當前
            let  p = path.join(this.config.dir,'.'+pathname)
         
            try{
               let statObj = await stat(p)//判斷p路徑對不對
                if(statObj.isDirectory()){

                }else{
                    //是文件就直接讀了
                    res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
                    fs.createReadStream(p).pipe(res)
                }
            }catch(e){
                res.statusCode = 404;
                res.end()
            }
        }
    }
複製代碼

這時候訪問 http://localhost:3000/index.html,就能出頁面信息了npm

  • 架子搭出來了,那麼就開始寫吧,由於報錯和展現頁面信息要重複利用,因此把他們單獨提出來封成兩個方法
sendFile(req,res,p){
        //是文件就直接讀了
        res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
        fs.createReadStream(p).pipe(res)
    }
    sendError(req,res,e){
        debug(util.inspect(e).toString())
        res.statusCode = 404;
        res.end()
    }
複製代碼
  • 假如訪問的是個目錄的話,咱們應該把目錄展現出來,這樣的話最好使用模板引擎,常見的模板引擎有:handlebar ejs 這咱們用的是ejs,用法 render(‘文件內容’,‘變量參數’),裝一下 :npm install ejs src/tmpl.ejs
<!DOCTYPE html>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="renderer" content="webkit">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <title>staticServer</title>
  </head>
  <body>
      <!-- 碰到js就 <%  %>包起來,求值用= -->
     <%dirs.forEach(dir=>{%>
         <li><a href="<%=dir.path%>"><%=dir.name%></a></li>
    <% })%>
  </body>
</html>
複製代碼

那在app.js中就要放進去數組

let ejs = require('ejs')
let tmpl = fs.readFileSync(path.join(__dirname,'tmpl.ejs'),'utf8')
let readDir = util.promisify(fs.readdir)//讀取目錄用的方法
複製代碼

git上各類模板在這裏

再把tmpl掛在this上 app.js那麼若是是目錄的話這個代碼就這麼寫

if(statObj.isDirectory()){
                    //若是是目錄的話就應該把目錄放出去
                    //用模板引擎寫 handlebal ejs underscore jade 
                    let dirs = await readDir(p)
                    debug(dirs)//返回的是個數組[index.css,index.html]
                   dirs = dirs.map(dir => ({
                        path: path.join(pathname, dir),
                        name: dir
                    }))
                let content = ejs.render(this.tmpl,{dirs})
                    res.setHeader('Content-Type','text/html;charset=utf8')
                    res.end(content)
                }else{
                    this,this.sendFile(req,res,p)
                }
複製代碼
  • 下面就是細化的問題了,總共3個方向:
  1. 若是文件訪問過,就應該有緩存的功能,
  2. 文件很大應該有壓縮,
  3. 範圍請求

緩存

工做原理

第一次請求:

1,客戶端發起 HTTP GET 請求一個文件。
2,服務器處理請求,返回文件內容以及相應的 Header,其中包括 Etag(例如:627-4d648041f6b80)(假設服務器支持 Etag 生成並已開啓了 Etag)狀態碼爲 200。
複製代碼

第二次請求(斷點續傳):

1,客戶端發起 HTTP GET 請求一個文件,同時發送 If-Range(該頭的內容就是第一次請求時服務器返回的 Etag:627-4d648041f6b80)。
2,服務器判斷接收到的 Etag 和計算出來的 Etag 是否匹配,若是匹配,那麼響應的狀態碼爲 206;不然,狀態碼爲 200。
複製代碼
cache(req,res,statObj){
        //etag if-none-match
        //Last-Modified  if-modified-since
        //Cache-Control 
        //ifNoneMatch通常是內容的md5戳 => ctime+size
        let ifNoneMatch = req.headers['if-none-match']
        //ifModifiedSince文件的最新修改時間
        let ifModifiedSince = req.headers['if-modified-since']
        let since = statObj.ctime.toUTCString();//最新修改時間
        //表明的是服務器文件的一個描述
        let etag = new Date(since).getTime()  +'-'+statObj.size
        res.setHeader('Cache-Control','max-age=10') 
        //10秒以內強制緩存
        res.setHeader('Etag',etag)
        res.setHeader('Last-Modified',since) //請求頭帶着
        //再訪問的時候對比,若是相等,就走緩存
        if(ifNoneMatch !== etag){
            return false
        }
        if(ifModifiedSince != since){
            return false
        }
        res.statusCode = 304
        res.end()
        return true

    }
複製代碼

sendFile 中加這句話

//緩存
        if(this.cache(req,res,statObj)) return
複製代碼

那麼訪問index.html 訪問的畫面是

這裏寫圖片描述
查看他們的頭
這裏寫圖片描述
固然再刷新的話200就會變成304,走的是緩存了 壓縮 由於用到了zlib因此要在頭上加上

let zlib = require('zlib');
複製代碼

壓縮方法

compress(req,res,statObj){
          // 壓縮 Accept-Encoding: gzip,deflate,br
        // Content-Encoding:gzip
       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 //不支持壓縮
        }
       }else{
           return false
       }
       
    }
複製代碼

sendFile

sendFile(req,res,p,statObj){
        //緩存
        if(this.cache(req,res,statObj)) return
        //壓縮
        let s = this.compress(req, res, p, statObj);
        console.log(s)
        res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
         let rs = fs.createReadStream(p)
        if(s){
            //若是支持就是返回的流
            rs.pipe(s).pipe(res)
        }else{
            rs.pipe(res)
        }
        //是文件就直接讀了
     
      //  fs.createReadStream(p).pipe(res)
    }
複製代碼

查看一下是否壓縮成功:訪問http://localhost:3000/index.css

這裏寫圖片描述
能夠看到支持的是gzip,成功! 範圍請求 方法

range(req,res,statObj){
        //範圍請求的頭 :Rang:bytes=1-100
        //服務器 Accept-Ranges:bytes
        //Content-Ranges:1-100/total
        let header = req.headers['range']
        //header =>bytes=1-100
        let start = 0;
        let end = statObj.size;//整個文件的大小
        if(header){
            res.setHeader('Content-Range','bytes')
            res.setHeader('Accept-Ranges',`bytes ${start}-${end}/${statObj.size}`)
            let [,s,e] = header.match(/bytes=(\d*)-(\d*)/);
            start = s?parseInt(s):start
            end = e? parseInt(e):end

        }
        return {start,end:end-1}//由於start是從0開始

    }
複製代碼

sendFile 文件就是這樣了

sendFile(req, res, p, statObj) {
        // 緩存的功能 對比 強制
        if (this.cache(req, res, statObj)) return;
        // 壓縮 Accept-Encoding: gzip,deflate,br
        // Content-Encoding:gzip
        res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
        let s = this.compress(req, res, p, statObj);
        // 範圍請求 
        let {start,end} = this.range(req,res,statObj);
        let rs = fs.createReadStream(p,{start,end})
        if (s) {
            rs.pipe(s).pipe(res);
        } else {
            rs.pipe(res);
        }
    }
複製代碼

在命令行工具下執行
curl -v --header "Range:bytes=1-3" http://localhost:3000/index.html 就能夠看到效果了

這裏寫圖片描述
若是你的window不能執行curl 能夠看這裏

git地址

點個贊再走嘛

相關文章
相關標籤/搜索