靜態資源服務器

node 靜態資源服務器

用node實現一個靜態資源服務器,讀取一個目錄下的文件,若是是目錄,顯示該目錄下的文件名。支持命令設置端口和靜態目錄,支持防盜鏈、緩存等功能。javascript

建立目錄 staticServer

  • npm init 在package.json里加入以下代碼。bin裏面是相應的命令,執行 static-server,即執行bin下面的www.js文件
"bin": {
    "static-server": "./bin/www.js"
  }
複製代碼

config

  • 在config目錄下建立index.js,配置文件,root是靜態目錄,port是端口號。
module.exports = {
    root:'public',
    port:8080
};
複製代碼

bin目錄

  • 在bin目錄下建立www.js
  • yargs是一個獲取命令行參數的庫
#! /usr/bin/env node
const argv = require('yargs')
    .usage('static-server')
    .options('port', {
        alias: 'p',
        describe: '設置端口號',
        default: 8080
    })
    .options('root', {
        alias: 'r',
        describe: '設置靜態目錄',
        default: process.cwd()
    })
    .help()
    .argv;
const config = Object.assign(require('../config'),argv);
let StaticServer =  require('../src');
let server = new StaticServer(config);
複製代碼

src

  • src是源代碼,template裏是模板文件,用來展現讀取的目錄信息,index是node服務的文件
  • 要用到獲取文件信息、讀取文件和目錄,把異步的方便變成同步的寫法,用到了util.promisify
const fsStat = util.promisify(fs.stat);
const fsReadFile = util.promisify(fs.readFile);
const fsReaddir = util.promisify(fs.readdir);
複製代碼

目錄模版

  • 讀取模版文件,用Handlebars作模版引擎
let template;
function getTemplate() {
    fs.readFile(path.join(__dirname, 'template', 'template.html'), 'utf8', (err, html) => {
        if (err) {
            console.log(err);
        } else {
            template = Handlebars.compile(html);
        }
    });
}
複製代碼

StaticServer類

  • 建立staticServer類,cacheType是緩存的類型,cwd是工做目錄
class StaticServer {
    constructor(config) {
        this.cacheType = ['CacheControl', 'Expires', 'LastModified', 'ETag'];
        this.port = config.port;
        this.root = config.root;
        this.cwd = process.cwd();
        this.createServer();
    }
}
複製代碼

createServer啓動服務

createServer() {
    try {
        const server = http.createServer(this.requestListener.bind(this));
        server.listen(this.port, () => {
            console.log(`server is ok;http://localhost:${this.port}`);
        });
    } catch (e) {
        this.errorListener(e);
    }
}
複製代碼

errorListener

  • errorListener容錯處理函數
errorListener(err) {
   console.log(err);
}
複製代碼

requestListener

  • createServer監聽函數,req是請求、res是相應、dir是文件目錄。
  • 用mime模塊得到mimetype,設置相應頭 Content-Type,若是是文字,設置charset是utf-8。
  • fsStat獲取文件的信息。若是是目錄,獲取目錄;若是是文件,返回文件。
  • 若是是圖片,先設置防盜鏈,返回文件。
async requestListener(req, res) {
    const pathname = url.parse(req.url).pathname;
    const dir = path.join(this.root, pathname);
    try {
        let contentType = mime.getType(dir);
        if (contentType&&contentType.match('text')) {
            contentType += ';charset=utf-8';
        }
        res.setHeader('Content-Type', contentType);
        const stat = await fsStat(dir);
        if (stat.isDirectory()) {
            this.getDir(req,res,pathname,dir);
        } else {
            this.proxyGetFile(req,res,pathname,dir,stat);
        }
    } catch (e) {
        res.statusCode = 404;
        res.end(e.toString());
        this.errorListener(e);
    }
}
複製代碼

讀取目錄

  • 循環目錄下的文件名,獲得一個數組,每一個元素是一個對象,名字和url。
  • 返回模板
async getDir(req,res,pathname,dir) {
    const dirs = await fsReaddir(dir);
    const list = dirs.map(dir => ({name: dir, url: path.join(pathname, dir)}));
    const html = template({
        title: dir,
        list
    });
    res.end(html);
}
複製代碼

防盜鏈

  • 若是是當前服務器訪問的就返回該圖片,若是是其餘服務器訪問,返回空白圖片
sendForbidden(req,res,pathname,dir,stat) {
    const referer = req.headers['referer'] || req.headers['refer'];
    if (referer && url.parse(referer).host !== req.headers['host']) {
        console.log('防盜鏈');
        res.statusCode = 403;
        fs.createReadStream(path.join(__dirname, 'forbidden.png')).pipe(res);
        return false;
    }
    return true;
}
複製代碼

讀取文件

  • 設置緩存,該類有getFileCacheControl、getFileExpires、getFileLastModified、getFileETag方法
proxyGetFile(req,res,pathname,dir,stat) {
    if (mime.getType(dir).match('image')) {
        //圖片
        if (!this.sendForbidden(req,res,pathname,dir,stat)) {
            return;
        }
    }
    for (let i = 0; i < this.cacheType.length; i++) {
        if (this[`getFile${this.cacheType[i]}`](req,res,pathname,dir,stat)) {
            console.log(this.cacheType[i]);
            return;
        }
    }
    this.getFile(req,res,pathname,dir,stat);
}
複製代碼

強制緩存

  • 經過設置響應頭Expires和Cache-Control來實現強制緩存
getFileExpires(req,res,pathname,dir,stat) {
    res.setHeader('Expires', new Date(Date.now() + 60 * 1000));
}

getFileCacheControl(req,res,pathname,dir,stat) {
    res.setHeader('Cache-Control', 'max-age=60');
}  
複製代碼

協商緩存

  • 設置響應頭的Last-Modified爲文件修改時間,若是請求頭的If-Modified-Since和文件修改時間同樣,設置Status Code爲304,讓客戶端從緩存裏獲取數據。
  • 設置響應頭的ETag爲文件修改時間的md5值,若是請求頭的If-None-Match和md5值同樣,設置Status Code爲304,讓客戶端從緩存裏獲取數據。
getFileLastModified(req,res,pathname,dir,stat) {
    const lastModified = stat.ctime.toGMTString();
    res.setHeader('Last-Modified', lastModified);
    if (req.headers['if-modified-since'] === lastModified) {
        res.statusCode = 304;
        res.end();
        return true;
    }
}

getFileETag(req,res,pathname,dir,stat) {
    let ifNoneMatch = req.headers['if-none-match'];
    let etag = crypto.createHash('md5').update(stat.ctime.toGMTString(), 'utf8').digest('hex');
    res.setHeader('ETag', etag);
    if (ifNoneMatch == etag) {
        res.statusCode = 304;
        res.end();
        return true;
    }
}
複製代碼

壓縮

  • 根據請求頭Accept-Encoding,返回不一樣的壓縮格式
compressFile(req,res,inp) {
    const acceptEncodings = req.headers['accept-encoding'];
    if(/\bgzip\b/.test(acceptEncodings)){
        this.gzipFile(req,res,inp);
        return true;
    }else if(/\bdeflate\b/.test(acceptEncodings)){
        this.deflateFile(req,res,inp);
        return true;
    }
    return false;
}

gzipFile(req,res,inp) {
    const gzip = zlib.createGzip();
    res.setHeader('Content-Encoding','gzip');
    inp.pipe(gzip).pipe(res);
}

deflateFile(req,res,inp) {
    const deflate = zlib.createDeflate();
    res.setHeader('Content-Encoding','deflate');
    inp.pipe(deflate).pipe(res);
}
複製代碼

讀取文件

  • 先進行壓縮
getFile(req,res,pathname,dir,stat) {
    const fsReadStream = fs.createReadStream(dir);
    if(!this.compressFile(req,res,fsReadStream))
        fsReadStream.pipe(res);
}
複製代碼
總結

根據近段時間學習,作了靜態資源服務器,根據請求頭作一些操做,返回響應的響應頭。功能有待改善,後續會繼續更新,併發不到npm上。html

源碼
相關文章
相關標籤/搜索