关于实现一个Node.js静态服务器你所需要知道的ALL

阅前须知

此文正在不断完善,阅读请谨慎

设计思路

当你输入一个url时,这个url可能对应服务器上的一个资源(文件)也可能对应一个目录。
So服务器会对这个url进行分析,针对不同的情况做不同的事。
如果这个url对应的是一个文件,那么服务器就会返回这个文件。
如果这个url对应的是一个文件夹,那么服务器会返回这个文件夹下包含的所有子文件/子文件夹的列表。
以上,就是一个静态服务器所主要干的事。

但真实的情况不会像这么简单,
我们所拿到的url可能是错误的,它所对应的文件或则文件夹或许根本不存在,
又或则有些文件和文件夹是被系统保护起来的是隐藏的,我们并不想让客户端知道。
因此,我们就要针对这些特殊情况进行一些不同的返回和提示。

再者,当我们真正返回一个文件前,我们需要和客户端进行一些协商。
我们需要知道客户端能够接受的语言类型、编码方式等等以便针对不同浏览器进行不同的返回处理。
我们需要告诉客户端一些关于返回文件的额外信息,以便客户端能更好的接收数据:
文件是否需要缓存,该怎样缓存?
文件是否进行了压缩处理,该以怎样的方式解压?
等等...

至此,我们已经初步了解了一个静态服务器所主要做的几乎所有事情,
let's go!

实现

项目目录

static-server/
|
| - bin/
|   | - start   # 批处理文件
|      
|
| - src/
|   | - App.js    # main文件
|   | - Config.js   # 默认配置
|
|
·- package.json

配置文件

要启动一个服务器,我们需要知道这个服务器的启动时的端口号和静态服务器的工作目录

let config = {
    host:'localhost' //提升用
    ,port:8080 //服务器启动时候的默认端口号
    ,path:path.resolve(__dirname,'..','test-dir') //静态服务器启动时默认的工作目录
}

整体框架

注意

  • 事件函数中的this默认指向绑定的对象(这里是小server),这里修改成了Server这个大对象,以便调用在回调函数中调用Server下的方法。
class Server(){
    constructor(options){
        /* === 合并配置参数 === */
        
        this.config = Object.assign({},config,options)
    }
    
    start(){
        /* === 启动http服务 === */
        
        let server = http.createServer();
        server.on('request',this.request.bind(this));  
        server.listen(this.config.port,()=>{
            let url =  `${this.config.host}:${this.config.port}`;
            console.log(`server started at ${chalk.green(url)}`)
        })
    }
    
    async request(req,res){
        /* === 处理客户端请求,决定响应信息 === */
        // try
        //如果是文件夹 -> 显示子文件、文件夹列表
        //如果是文件 -> sendFile()
        // catch
        //出错 -> sendError()
    }
    
    sendFile(){
        //对要返回的文件进行预处理并发送文件
    }
    
    handleCache(){
        //获取和设置缓存相关信息
    }
    
    getEncoding(){
        //获取和设置编码相关信息
    }
    
    getStream(){
        //获取和设置分块传输相关信息
    }
    
    sendError(){
        //错误提示
    }
}

module.exports = Server;

request请求处理

获取url的pathname,和服务器本地的工作根目录地址进行拼接,返回一个filename
利用filename和stat方法检测是文件还是文件夹

是文件夹,
利用readdir方法返回该文件夹下的列表,将列表包装成一个对象组成的数组
然后结合handlebar将数组数据编译到模板中,最后返回这个模板给客户端

是文件,
将req、res、statObj、filepath传递给sendFile,接下来交由sendFile处理

async request(req,res){
    let pathname = url.parse(req.url);
    if(pathname == '/favicon.ico') return;
    let filepath = path.join(this.config.root,pathname);
    try{
        let statObj = await stat(filepath);
        if(statObj.isDirectory()){
            let files = awaity readdir(filepath);
            files.map(file=>{
                name:file
                ,path: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){
        this.sendError(e,req,res);
    }
}

[tip] 我们将request方法async化,这样我们就能像写同步代码一样写异步

方法

sendFile

涉及缓存、编码、分段传输等功能

sendFile(){
    if(this.handleCache(req,res,filepath,statObj)) return; //如果走缓存,则直接返回。
    res.setHeader('Content-type',mime.getType(filepath)+';charset=utf-8');
    let encoding = this.getEncoding(req,res); //获取浏览器能接收的编码并选择一种
    let rs = this.getStream(req,res,filepath,statObj); //支持断点续传
    if(encoding){
        rs.pipe(encoding).pipe(res);
    }else{
        rs.pipe(res);
    }
}

handleCache

缓存处理时要注意的是,缓存分为强制缓存和对比缓存,且强制缓存的优先级是高于相对缓存的。
也就是说,当强制缓存生效的时候并不会走相对缓存,不会像服务器发起请求。
但一旦强制缓存失效,就会走相对缓存,如果文件标识没有改变,则相对缓存生效,
客户端仍然会去缓存数据拿取数据,所以强制缓存和相对缓存并不冲突。
强制缓存和相对缓存一起使用时,能在减少服务器的压力的同事又保持请求数据的及时更新。

另外需要注意的是,如果同时设置了两种相对缓存的文件标识,必须要两种都没有改变时,缓存才生效。

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');
    res.setHeader('Expires',new Date(Date.now()+30*1000).toGMTString()); //此时间必须为GMT
    
    let etag = statObj.size;
    let lastModified = statObj.ctime.toGMTString(); //此时间格式可配置
    res.setHeader('Etag',etag);
    res.setHeader('Last-Modified',lastModified);
    
    if(isNoneMatch && isNoneMatch != etag) return false; //若是第一次请求已经返回false
    if(ifModifiedSince && ifModifiedSince != lastModified) return false;
    if(isNoneMatch || ifModifiedSince){
    // 说明设置了isNoneMatch或则isModifiedSince且文件没有改变
        res.writeHead(304);
        res.end();
        return true;
    }esle{
        return false;
    }
}

getEncoding

从请求头中拿取到浏览器能接收的编码类型,利用正则匹配匹配出最前面那个,
创建出对应的zlib实例返回给sendFile方法,以便在返回文件时进行编码。

getEncoding(req,res){
    let acceptEncoding = req.headers['accept-encoding'];
    if(/\bgzip\b/.test(acceptEncoding)){
        res.setHeader('Content-Encoding','gzip');
        return zlib.createGzip();
    }else if(/\bdeflate\b/.test(acceptEncoding)){
        res.setHeader('Content-Encoding','deflate');
        return zlib.createDeflate();
    }else{
        return null;
    }
}

getStream

分段传输,主要利用的是请求头中的req.headers['range']来确认要接收的文件是从哪里开始到哪里结束,然而真正拿到这部分数据是通过fs.createReadStream来读取到的。

getStream(req,res,filepath,statObj){
    let start = 0;
    let end = startObj.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])?0:parseInt(result[1]);
            end = isNaN(result[2])?end:parseInt(result[2])-1; //因为readstream的索引是包前又包后故要减去1
        }
    }
    return fs.createReadStream(filepath,{
        start,end
    });
}

包装成命令行工具

我们可以像在命令行中输入npm start启动一个dev-server一样自定义一个启动命令来启动我们的静态服务器。

大体实现的思路是:
packge.json中的bin属性下配置一个启动命令和这个执行这个命令的文件的路径。
然后我们需要准备一个批处理文件,在文件中引入我们的静态服务器文件,让我们的服务器跑起来
然后将这个文件node link即可。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,112评论 18 139
  • 上一篇《WEB请求处理一:浏览器请求发起处理》,我们讲述了浏览器端请求发起过程,通过DNS域名解析服务器IP,并建...
    七寸知架构阅读 80,577评论 21 356
  • 一个可以取得较大成功的人,他一定懂得如何培养好的习惯来代替坏的习惯,当好的习惯积累多了,他的效率就会高出其他人,因...
    小木童阅读 130评论 0 0
  • 材料:铁丝 粗 细 拉菲草 粗铁丝缠上拉菲草 中间对折,根部缠绕 平均分开, 中间缠绕 如图 外...
    花艺先生阅读 909评论 1 3
  • 总在盼望,总在失望,日子不就是那样,过得不情不愿 各有各的生活,你不必难过,你有你的生活,他迟早属于别人的,只是你...
    馨陌阅读 140评论 0 0