Node之手写静态资源服务器

背景

学习服务端知识,入门就是要把文件挂载到服务器上,我们才能去访问相应的文件。本地开发的时候,我们也会经常把文件放在服务器上去访问,以便达到在同一个局域网内,通过同一个服务器地址访问相同的文件,比如我们会用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通过时间进行强缓。

  1. 对文件进行压缩,压缩文件以后可以减少体积,加快传输速度和节约带宽 ,这里支持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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,493评论 18 399
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,678评论 6 342
  • 夜晚,没有什么事做,为了让蜗居一天的身体得以舒展,和夫向河畔信步。 新年的第一天,河畔管理处定是一个人在值班,...
    老丁子阅读 545评论 17 7
  • 演讲题目:基于智能手机的迷你机器人 讲师:Keller Rinaudo, 讲师介绍:Keller Rinaudo ...
    木唯蔓阅读 210评论 1 0