實現一個簡易的靜態伺服器

風光好採光發表於2018-08-14

靜態伺服器就是網站把一些在通常操作下不會發生改變的資源給瀏覽器。顯示網站外觀的圖片和CSS檔案,在瀏覽器中執行的JavaScript程式碼,沒有動態元件的HTML檔案就是這種資源中的代表,統稱為靜態檔案。

這裡我用node來開發一個伺服器,提供的其他功能有:

  1. 快取
  2. 壓縮
  3. 解析使用者命令列輸入

我們會用到的node包有:http,fs,path,url,zlib

http是提供web服務的核心包

fs是檔案或資料夾操作包

path是將請求的路徑轉換成本機路徑

url是將請求的網址轉換成物件,方便我們呼叫

zlib是壓縮包
複製程式碼

我們還會用到一些第三方包:ejs,mime,commander

ejs是一個javascript的模板引擎

mime是檔案型別判斷包

commander用來解析使用者在命令列輸入的引數
複製程式碼

我們的靜態伺服器的實現思路就是,啟動一個靜態伺服器,監聽使用者傳送的請求,當請求到來時,解析拿到請求的地址。如果請求的是檔案,就讀取相應的檔案並返回給使用者。如果的資料夾,就讀取資料夾下的所有檔名,然後把檔名放在模板html裡並返回給使用者。

class Server{
    constructor(opts = {}){
        this.host = opts.host || '127.0.0.1'
        this.port = opts.port || 3000
        this.staticPath = opts.staticPath || 'public'
    }
    start(){
        let server = http.createServer(this.handleRequest.bind(this));

        server.listen(this.port, this.host, ()=>{
            console.log(`服務已啟動:${this.host}:${this.port}`)
        })
    }
    handleRequest(req, res){
        let self = this;

        //獲取請求的檔名
        let {pathname} = url.parse(req.url);
        if(pathname == '/favicon.ico') return res.end();

        //把請求的檔名轉換成public下的絕對路徑
        let p = path.join(__dirname, '../', this.staticPath, pathname);

        fs.stat(p, function(err, stats){
            if(err){
                res.end(err);
            }

            if(stats.isDirectory()){
                self.sendDir(req, res, stats, p);
            }else{
                self.sendFile(req, res, stats, p);
            }
        })
    }
    sendFile(req, res, stats, p){
        res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8')//傳送的資料型別
        
        fs.createReadStream(p).pipe(res)
    }

    sendDir(req, res, stats, p){
        let {pathname} = url.parse(req.url);

        res.setHeader('Content-Type', 'text/html;charset=utf-8')//傳送的資料型別
        let template = fs.readFileSync(path.join(__dirname, 'template.html'), 'utf-8')
        let files = fs.readdirSync(p)
        files = files.map(file=>{
            return {
                filename: file,
                filepath: path.join(pathname, file)
            }
        })
        let str = ejs.render(template, {
            name:`index of ${pathname}`, 
            arr:files,
        })

        res.end(str)
    }
}
複製程式碼

template.html就是用來顯示資料夾下的檔名,主要用到的就是ejs模板引擎,內容為

<h2><%=name%></h2>
<%arr.forEach(item=>{%>
    <li><a href="<%=item.filepath%>"><%=item.filename%></a></li>
<%})%>
複製程式碼

快取功能也很簡單,就是設定一些響應頭,給Server類增加一個原型方法

setCache(req, res, stats, p){
    res.setHeader('Cache-Control', 'max-age=10')//快取存活時間
    res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString())//快取存活時間
    
    let etag = stats.ctime.getTime() + '-' + stats.size;
    let LastModified = stats.ctime.toGMTString();
    
    let ifNoneMatch = req.headers['if-none-match']
    let ifModifiedSince = req.headers['if-modified-since']//檔案最後修改時間

    res.setHeader('Last-Modified', LastModified)
    res.setHeader('Etag', etag)
    
    if(etag == ifNoneMatch && LastModified == ifModifiedSince){
        return true;
    }

    return false;
}
複製程式碼

在傳送檔案方法裡開啟快取

sendFile(req, res, stats, p){
    res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8')//傳送的資料型別
    
    if(this.setCache(req, res, stats, p)){
        res.statusCode = 304;
        return res.end();
    }

    fs.createReadStream(p).pipe(res)
}
複製程式碼

壓縮功能跟快取類似,增加方法

gzip(req, res, stats, p){
    let encoding = req.headers['accept-encoding']

    if(encoding){
        if(encoding.match(/\bgzip\b/)){
            res.setHeader('Content-Encoding', 'gzip')//壓縮型別
            return zlib.createGzip();
        }
        if(encoding.match(/\bdeflate\b/)){
            res.setHeader('Content-Encoding', 'deflate')//壓縮型別
            return zlib.createDeflate();
        }

        return false;
    }else{
        return false;
    }
}
複製程式碼

開啟壓縮功能

sendFile(req, res, stats, p){
    res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8')//傳送的資料型別
    
    if(this.setCache(req, res, stats, p)){
        res.statusCode = 304;
        return res.end();
    }

    let transform = this.gzip(req, res, stats, p);
    if(transform){
        return fs.createReadStream(p).pipe(transform).pipe(res)
    }

    fs.createReadStream(p).pipe(res)
}
複製程式碼

至此我們的主要功能就實現了,但是我們的服務不夠智慧,比如埠固定是3000,這樣會出現埠衝突的問題。我們可以用commander包來接收使用者的配置,來動態修改埠。

let program = require('commander')

program
    .option('-p,--port <n>', 'config port')
    .option('-o,--host [value]', 'config host')

program.parse(process.argv);
複製程式碼

啟動服務的時候把program傳進去就可以了

let server = new Server(program);
server.start();
複製程式碼

相關文章