Node之手寫靜態資源伺服器

張淼發表於2018-03-01

背景

學習服務端知識,入門就是要把檔案掛載到伺服器上,我們才能去訪問相應的檔案。本地開發的時候,我們也會經常把檔案放在伺服器上去訪問,以便達到在同一個區域網內,通過同一個伺服器地址訪問相同的檔案,比如我們會用xampp,會用sulime的外掛sublime-server等等。本篇文章就通過node,手寫一個靜態資源伺服器,以達到你可以隨意定義任何一個資料夾為根目錄,去訪問相應的檔案,達到anywhere is your static-server。

主要實現功能

  1. 讀取靜態檔案
  2. 靜態資源快取
  3. 資源壓縮
  4. MIME型別支援
  5. 斷點續傳
  6. 釋出為可執行命令並可以後臺執行,可以通過npm install -g安裝

Useage

//install
$ npm i st-server -g
//forhelp
$ st-server -h
//start
$ st-server
// or with port
$ st-server -p 8800
// or with hostname
$ st-server -o localhost -p 8888
// or with folder
$ st-server -d / 
// full parameters
$ st-server -d / -p 9900 -o localhost
複製程式碼

其中可以配置三個引數,-d代表你要訪問的根目錄,-p代表埠號(目前暫不支援多次開啟用同一個埠號,需要手動殺死之前的程式),-o代表hostname。 所有原始碼已經上傳至github

原始碼分析

  • 全部程式碼基於一個StaticServer類進行實現,在建構函式中首先引入所有的配置,argv是通過命令列敲入傳進來的引數,然後在獲取需要編譯的模板,該模板是簡單的顯示一個資料夾下所有檔案的列表。基於handlebars實現。然後開啟服務,監聽請求,由this.request()處理
class StaticServer{
    constructor(argv){
        this.config = Object.assign({},config,argv);
        this.compileTpl = compileTpl();
    }
    startServer(){
        let server = http.createServer();
        server.on('request',this.request.bind(this));
        server.listen(this.config.port,()=>{
            let serverUrl = `http://${this.config.host}:${this.config.port}`;
            debug(`服務已開啟,地址為${chalk.green(serverUrl)}`);
        })
    }
}
複製程式碼
  • 主線就是讀取想要搭建靜態服務的地址,如果是資料夾,則查詢該資料夾下是否有index.html檔案,有則顯示,沒有則列出所有的檔案;如果是檔案的話,則直接顯示該檔案內容。大前提在顯示具體的檔案之前,要判斷有沒有快取,有直接獲取快取,沒有的話再請求伺服器。
 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);
        let statObj = await fsStat(filePath);
        if(statObj.isDirectory()){//如果是一個目錄的話 列出目錄下面的內容
            let files = await readDir(filePath);
            let isHasIndexHtml = false;
            files = files.map(file=>{
                if(file.indexOf('index.html')>-1){
                    isHasIndexHtml = true;
                }
                return {
                    name:file,
                    url:path.join(pathname,file)
                }
            })
            if(isHasIndexHtml){
                let statObjN = await fsStat(filePath+'/index.html');
                return this.sendFile(req,res,filePath+'/index.html',statObjN);
            }
            let resHtml = this.compileTpl({
                title:filePath,
                files
            })
            res.setHeader('Content-Type','text/html');
            res.end(resHtml);
        }else{
            this.sendFile(req,res,filePath,statObj);
        }
        
    }
    sendFile(req,res,filePath,statObj){
        //判斷是否走快取
        if (this.getFileFromCache(req, res, statObj)) return; //如果走快取,則直接返回
        res.setHeader('Content-Type',mime.getType(filePath)+';charset=utf-8');
        let encoding = this.getEncoding(req,res);
        //常見一個可讀流
        let rs = this.getPartStream(req,res,filePath,statObj);
        if(encoding){
            rs.pipe(encoding).pipe(res);
        }else{
            rs.pipe(res);
        }
    }
複製程式碼

sendFile方法就是向瀏覽器輸出內容的方法,主要包括以下幾個重要的點:

  1. 快取處理
 getFileFromCache(req,res,statObj){
        let ifModifiedSince = req.headers['if-modified-since'];
        let isNoneMatch = req.headers['if-none-match'];
        res.setHeader('Cache-Control','private,max-age=60');
        res.setHeader('Expires',new Date(Date.now() + 60*1000).toUTCString());
        let etag = crypto.createHash('sha1').update(statObj.ctime.toUTCString() + statObj.size).digest('hex');
        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.statusCode = 304;
            res.end('');
            return true;
        } else {
            return false;
        }
    }
複製程式碼

這裡我們通過Last-Modified,ETag實現協商快取,Cache-Control,Expires實現強制快取,當所有快取條件成立時才會生效。Last-Modified原理是通過檔案的修改時間,判斷檔案是否修改過,ETag通過檔案內容的加密判斷是否修改過。Cache-Control,Expire通過時間進行強緩。 2. 對檔案進行壓縮,壓縮檔案以後可以減少體積,加快傳輸速度和節約頻寬 ,這裡支援gzip和deflate兩種方式,用node本身的模組zlib進行處理。

  getEncoding(req,res){
        let acceptEncoding = req.headers['accept-encoding'];
        if(acceptEncoding.match(/\bgzip\b/)){
            res.setHeader('Content-Encoding','gzip');
            return zlib.createGzip();
        }else if(acceptEncoding.match(/\bdeflate\b/)){
            res.setHeader('Conetnt-Encoding','deflate');
            return zlib.createDeflate();
        }else{
            return null;
        }
    }
複製程式碼
  1. 通過range,進行斷點續傳的處理
 getPartStream(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]) - 1;
            }
        }
        return fs.createReadStream(filePath,{
            start,end
        })
    }
複製程式碼
  1. 生成命令列工具,用npm安裝yargs包進行操作,並在package.json中新增 "bin": { "st-Server": "bin/www" },指向需要執行命令的檔案,然後在www中配置對應的命令,並且開啟子程式進行主程式碼的操作,為了解決你開啟命令後,命令列一直處於卡頓的狀態。開啟子程式也是node原生模組child_process支援的。
#! /usr/bin/env node

let yargs = require('yargs');
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: 8800,
    description: '請配置埠號'
})
    .usage('st-server [options]')
    .example(
    'st-server -d / -p 9900 -o localhost', '在本機的9900埠上監聽客戶端的請求'
    ).help('h').argv;

let path = require('path');
let {
 spawn
} = require('child_process');

let p1 = spawn('node', ['www.js', JSON.stringify(argv)], {
 cwd: __dirname
});
p1.unref();
process.exit(0);
複製程式碼

參考

anywhere

相關文章