Node構建一個靜態檔案伺服器

yingzai發表於2018-03-20

靜態檔案伺服器實現的功能

讀取靜態檔案
MIME型別支援
快取支援/控制 
支援gzip壓縮 
Range支援,斷點續傳 
釋出為可執行命令並可以後臺執行,可以通過npm install -g安裝

 首先先構建好專案目錄,專案目錄如下:

project
 |---bin 命令列實現放置指令碼
 |
 |---public 靜態檔案伺服器預設靜態資料夾
 |
 |---src 實現功能的相關程式碼
 |   |
 |   |__template 模板資料夾
 |   |
 |   |__app.js 主要功能檔案(main檔案)
 |   |__config.js 配置檔案
 |
 |---package.josn (初始化)複製程式碼

要啟動一個伺服器,我們需要知道這個伺服器的啟動時的埠號,在config.js配置一下:  

let config = {
 host:'localhost' //提示用 ,
port:8080 //伺服器啟動時候的預設埠號,
path:path.resolve(__dirname,'..','test-dir') //靜態伺服器啟動時預設的工作目錄
 }
複製程式碼

讀取靜態檔案之前首先要先啟動伺服器,之後所有的方法都在class Server方法裡

//handlebar 編譯模板,得到一個渲染的方法,然後傳入實際資料資料就可以得到渲染後的HTML了
function list() {
let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'),'utf8');
return handlebars.compile(tmpl);//進行編譯,最後渲染
}class Server {
    constructor(argv) {
        this.list = list();
        this.config = Object.assign({}, this.config, argv);
    }
    start() {
        let server = http.createServer();//建立伺服器
        //當客戶端向服務端發出資料的時候,會出發request事件
        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)}`);
        });
    }
//傳送錯誤資訊,
sendError(err, req, res) {
    res.statusCode = 500;
    res.end(`${err.toString()}`);
}}
module.exports = Server;複製程式碼

讀取靜態檔案

設計思路

首先輸入一個url時,可能對應伺服器上的一個檔案,或者對應一個目錄,
檢查是否檔案還是目錄
如果檔案不存在,返回404狀態碼,傳送not found頁面到客戶端
如果檔案存在:
開啟檔案讀取
設定response header
傳送檔案到客戶端
如果是目錄就開啟目錄列表
async request(req, res) {
    //先取到客戶端想要的是檔案或資料夾路徑 
    let { pathname } = url.parse(req.url);//獲取路徑的檔案資訊
    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)
            }));
            //handlebar 編譯模板
            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);
    }
}複製程式碼

快取支援/控制 

設計思路

快取分為強制快取和對比快取: 

  • 兩類快取規則可以同時存在,強制快取優先順序高於對比快取,也就是說,當執行強制快取的規則時,如果快取生效,直接使用快取,不再執行對比快取規則. 
  • 強制快取如果生效,不需要再和伺服器發生互動,而對比快取不管是否生效,都需要與服務端發生互動
1. 第一次訪問伺服器的時候,伺服器返回資源和快取的標識,客戶端則會把此資源快取在本地的快取資料庫中。
 2. 第二次客戶端需要此資料的時候,要取得快取的標識,然後去問一下伺服器我的資源是否是最新的。如果是最新的則直接使用快取資料,如果不是最新的則伺服器返回新的資源和快取規則,客戶端根據快取規則快取新的資料。

通過最後修改時間來判斷快取是否可用

  1. Last-Modified:響應時告訴客戶端此資源的最後修改時間 
  2. If-Modified-Since:當資源過期時(使用Cache-Control標識的max-age),發現資源具有Last-Modified宣告,則再次向伺服器請求時帶上頭If-Modified-Since。
  3.  伺服器收到請求後發現有頭If-Modified-Since則與被請求資源的最後修改時間進行比對。若最後修改時間較新,說明資源又被改動過,則響應最新的資源內容並返回200狀態碼; 
  4. 若最後修改時間和If-Modified-Since一樣,說明資源沒有修改,則響應304表示未更新,告知瀏覽器繼續使用所儲存的快取檔案。

    ETag是資源標籤。如果資源沒有變化它就不會變。

  1. 客戶端想判斷快取是否可用可以先獲取快取中文件的ETag,然後通過If-None-Match傳送請求給Web伺服器詢問此快取是否可用。
  2.  伺服器收到請求,將伺服器的中此檔案的ETag,跟請求頭中的If-None-Match相比較,如果值是一樣的,說明快取還是最新的,Web伺服器將傳送304 Not Modified響應碼給客戶端表示快取未修改過,可以使用。
  3.  如果不一樣則Web伺服器將傳送該文件的最新版本給瀏覽器客戶端

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');//max-age=30快取內容將在30秒後失效
    res.setHeader('Expires', new Date(Date.now() + 30 * 1000).toGMTString());
    let etag = statObj.size;
    let lastModified = statObj.ctime.toGMTString();
    res.setHeader('ETag', etag);//獲取ETag
    res.setHeader('Last-Modified', lastModified);//伺服器檔案的最後修改時間
    //任何一個對比快取頭不匹配,則不走快取
    if (isNoneMatch && isNoneMatch != etag) {//快取過期
        return fasle;
    }
    if (ifModifiedSince && ifModifiedSince != lastModified) {//快取過期
        return fasle;
    }
    //當請求中存在任何一個對比快取頭,則返回304,否則不走快取
    if (isNoneMatch || ifModifiedSince) {//快取有效
        res.writeHead(304);
        res.end();
        return true;
    } else {
        return false;
    }
}複製程式碼

支援gzip壓縮 

設計思路

瀏覽器都會攜帶自己支援的壓縮型別,最常用的兩種是gzip和deflate。根據請求頭Accept-Encoding,返回不同的壓縮格式.
getEncoding(req, res) {

    let acceptEncoding = req.headers['accept-encoding'];//獲取客戶端傳送的壓縮請求頭的資訊
    if (/\bgzip\b/.test(acceptEncoding)) {//如果是gzip的格式
        res.setHeader('Content-Encoding', 'gzip');
        return zlib.createGzip();
    } else if (/\bdeflate\b/.test(acceptEncoding)) {//如果是deflate的格式
        res.setHeader('Content-Encoding', 'deflate');
        return zlib.createDeflate();
    } else {
        return null;//不壓縮
    }
}複製程式碼

Range支援,斷點續傳 

設計思路

  • 該選項指定下載位元組的範圍,常應用於分塊下載檔案
  • 伺服器告訴客戶端可以使用range response.setHeader('Accept-Ranges', 'bytes')
  •  Server通過請求頭中的Range: bytes=0-xxx來判斷是否是做Range請求,如果這個值存在而且有效,則只發回請求的那部分檔案內容,響應的狀態碼變成206,如果無效,則返回416狀態碼,表明Request Range Not Satisfiable  
getStream(req, res, filepath, statObj) {
    let start = 0;//可讀流起始位置
    let end = statObj.size - 1;//可讀流結束位置
    let range = req.headers['range'];//獲取客戶端的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]) - 1;
        }
    }
    return fs.createReadStream(filepath, {
        start, end
    });
}複製程式碼

釋出為可執行命令

首先在
package.json配置一下"bin": { "http-static": "bin/www" }

#! /usr/bin/env node     //這段程式碼一定要寫在開頭,為了相容各個電腦平臺的差異性
// -d --root 靜態檔案目錄 -o --host 主機 -p --port 埠號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:'localhost',  
  type:'string',    
description:'請配置監聽的主機'})
.option('p',{  
  alias:'root',  
  demand:'false',   
 type:'number',   
 default:8080,  
  description:'請配置埠號'})
.usage('http-static [options]').example(  
  'http-static -d / 8080 -o localhost','在本機的9090埠上監聽客戶端的請求'
).help('h').argv;
// argv = {d,root,o,host,p,port}let server = new Server(argv);//啟動服務server.start();
複製程式碼

這樣命令列當中通過輸入http-static來直接啟動靜態檔案伺服器了,那麼命令列呼叫的功能也就實現了,最後用npm publish釋出一下,釋出到npm上面去了,我們就可以通過npm install -g來進行全域性安裝了

相關文章