手把手教你如何实现一个基于node的静态文件服务器

首先来看看我要准备给大家写的静态文件服务器都要实现哪些功能,然后根据具体的功能我们来一一的介绍

支持以下功能

  • 支持输出日志debug
  • 读取静态文件
    • 文件的读取 MIME类型支持
    • 文件夹列表展示 handlebars模板引擎
  • 缓存支持/控制
  • Range支持,断点续传
  • 支持gzip和deflate压缩
  • 发布为可执行命令并可以后台运行,可以在全局通过下面的命令来设置根目录端口号和主机名:myselfsever -d指定静态文件的根目录 -p指定端口号 -o指定监听的主机

下载地址

启动

    npm install // 安装依赖
    npm link // 创建连接
    myselfserver // 在任意目录下启动服务
    // 访问 localhost:8080

其次我们先大致看一下我们的服务器的大致结构

image

接下来我们来介绍我们这个服务器

要想实现一个服务器的功能,我们往往需要设置一下服务器的主机名,端口号,静态文件根目录等信息,在这里我们的config.js帮我们实现了这一配置,当然了这是基础的配置,后面我们还会讲到如何通过命令行来更改配置。基础配置如下:

let path = require('path');
let config = {
    host: 'localhost',// 监听主机
    port: 8080,// 主机端口号
    root: path.resolve(__dirname, '..')// 静态文件根目录
}
module.exports = config;

配置好之后接下来我们就需要写一个我们的服务了,服务的代码会在我们的app.js中体现,其中包括我们上面列举的几乎所有的功能,我们先来看一下app.js的代码结构


    const fs = require('fs');
    const url = require('url');
    const http = require('http');
    const path = require('path');
    const config = require('./config');
    
    class Server {
        constructor(argv) {
            // 生产handlebars模板
            this.list = list()
            // 处理命令行中设置的参数,来重写基本参数
            this.config = Object.assign({}, config, argv)
        }
        start() {
            /*启动http服务*/
            let server = http.createServer();
            server.on('request', this.request.bind(this));
            let url = `http://${config.host}:${config.port}`;
    
            server.listen(this.config.port, ()=>{
                // (1)支持输出日志debug 在下文中会讲一下使用它的注意事项
                debug(`server started at ${chalk.green(url)}`);
            })
        }
        /*(2)读取静态文件*/
        async request(req,res) {
          // 因为响应可能出错,所以在我们的代码中这里会用try catch包一下。
          // try
          // 文件夹: (2.1)模板引擎渲染展示文件列表  这里用handlebars模板引擎来渲染
          // 文件:-->sendFile
          // catch
          // 处理错误交给-->sendError
        }
        /* send file to browser*/
        sendFile (req, res, filePath, statObj) {
            // (2.2)处理文件并发送给浏览器  添加MIME类型支持
        }
        /* handle error*/
        sendError (error, req, res) {
            // 公用的的错误处理函数
        }
        /*cache 是否走缓存*/
        isCache (req, res, filePath, statObj) {
            // (3)处理缓存
        }
        /*broken-point continuingly-transferring  断点续传*/
        rangeTransfer (req, res, filePath, statObj) {
            // (4)支持断点续传
        }
        /*compression 压缩*/
        compression (req, res) {
           // (5)支持gzip和deflate压缩
        }
    }
    module.exports = Server;
    (6)myselfsever -d指定静态文件的根目录 -p指定端口号 -o指定监听的主机 这个命令实现的脚本代码在 bin目录下的commond文件中

开始进入细节分析阶段

注:每用到一个模块的时候记得提前安装到本地

(1)如何打印错误日志---debug


const debug = require('debug'); // 安装引入debug包
debug('staticserver:app');
// debug 第三方模块返回的是一个函数,该函数执行需要传入两个参数,一个是你当前项目的名称,即package.json 中的name值,第二个参数是你的模块的服务入口文件的名称。在咱们的这个项目中是app.js

当我们项目中要使用debug错误输出模块时,需要配置环境变量我们的debug日志才会被输出

环境变量的配置方法
windows上配置环境变量

  • 配置单个文件,即只有当前文件中的debug日志会输出 (优势:可以灵活的配置,输出自己想要输出的文件的debug日志)
   $ set DEBUG=staticserver:app
  • 配置整个项目,整个项目中的debug日志都会被输出
   $ set DEBUG=staticserver:*

mac上配置环境变量

  $ export DEBUG=staticserver:app
  $ export DEBUG=staticserver:*

(2)读取静态文件

如果是文件夹的话显示文件列表,这里我们用到了handlebars模板引擎

    const handlebars = require('handlebars'); // 模版引擎
    // 调用handlebars.compile方法生成一个模板
    function list () {
        let tmpl = fs.readFileSync(path.resolve(__dirname,'template','list.html'),'utf8');
        return handlebars.compile(tmpl)
    }

接下来我们来看访问的路径是文件的情况。这时候我们会根据文件的后缀的不同返回不同的响应类型,这时我们就用到了我们的mime模块


    /*静态文件服务器*/
    const { promisify, inspect } = require('util');
    const mime = require('mime');
    const stat = promisify(fs.stat);
    const readdir = promisify(fs.readdir);
    async request(req,res) {
        // 先取到客户端想访问的路径
        let { pathname } = url.parse(req.url);
        // 如果访问的是favicon.ico 的话返回一个错误信息
        if (pathname == '/favicon.ico') {
            return this.sendError('not favicon.ico',req,res)
        }
        // 获取访问的文件或文件夹的路径
        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)
               }))
                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)); // 把一个对象转化为字符串,因为有的tosting会生成object object
            this.sendError(e, req, res);
        }
    }

    // 接下来我们来看访问的路径是文件的情况。这时候我们会根据文件的后缀的不同返回不同的响应类型,这时我们就用到了我们的mime模块
  
      sendFile (req, res, filePath, statObj) {
        // 如果缓存存在的话走缓存
        if(this.isCache(req, res, filePath, statObj)){
            return;
        }
        res.statusCode = 200; // 可以省略
        res.setHeader('Content-Type', mime.getType(filePath) + ';charset=utf-8');
        let encoding = this.compression(req,res);
        // 是不是需要压缩
        if(encoding) {
            // 在这里使用断点续传
            this.rangeTransfer(req, res, filePath, statObj).pipe(encoding).pipe(res)
        } else {
           
            this.rangeTransfer(req, res, filePath, statObj).pipe(res)
        }
    }

(3)缓存支持和控制

要想理解下面缓存是否存在,缓存是否有效来判断要不要走缓存,大家可以先看一下这篇文章node中的缓存机制可以加深对缓存的理解


    /*cache 是否走缓存*/
    isCache (req, res, filePath, statObj) {
        let ifNoneMatch = req.headers['if-none-match'];
        let ifModifiedSince = req.headers['if-modified-since'];
        res.setHeader('Cache-Control','private,max-age=10');
        res.setHeader('Expires',new Date(Date.now() + 10*1000).toGMTString);
        let etag = statObj.size;
        let lastModified = statObj.ctime.toGMTString();
        res.setHeader('Etag',etag)
        res.setHeader('Last-Modified',lastModified);
        if(ifNoneMatch && ifNoneMatch != etag) {
            return false
        }

        if(ifModifiedSince && ifModifiedSince != lastModified){
            return false
        }
        if(ifNoneMatch || ifModifiedSince) {
            res.writeHead(304);
            res.end();
            return true
        } else {
            return false
        }
    }

(4)Range支持,断点续传

该选项指定下载字节的范围,常应用于分块下载文件

range的表示方式有多种,如100-500,则指定从100开始的400个字节数据;-500表示最后的500个字节;5000-表示从第5000个字节开始的所有字节

另外还可以同时指定多个字节块,中间用","分开

服务器告诉客户端可以使用range response.setHeader('Accept-Ranges', 'bytes')

Server通过请求头中的Range:bytes=0-xxx来判断是否是做Range请求,如果这个值存在而且有效,则只发回请求的那部分文件内容,响应的状态码变成206,如果无效,则返回416状态码,表明Request Range Not Satisfiable


     /*broken-point continuingly-transferring  断点续传*/
    rangeTransfer (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*)/);
            start = isNaN(result[1]) ? start : parseInt(result[1]);
            end = isNaN(result[2]) ? end : parseInt(result[2]) - 1
        }
        return fs.createReadStream(filePath, {
            start,
            end
        })
    }

(5)支持gzip和deflate压缩

服务器会根据客户端请求中的req.headers['accept-encoding']这个字段中的值来判断客户端支持哪种解压类型来进行压缩的

    /*compression 压缩*/
    compression (req, res) {
        let acceptEncoding = req.headers['accept-encoding'];//163.com
        if(acceptEncoding) {
            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
            }
        }
    }

(6)发布为可执行命令并可以后台运行,可以在全局通过下面的命令来设置根目录端口号和主机名:myselfsever -d指定静态文件的根目录 -p指定端口号 -o指定监听的主机

这个功能是在我们的bin目录下面的command 文件中实现的。
文件开头的#! /usr/bin/env node 这句命令是告诉该脚本用哪种程序来执行,详细可看这边文章Node.js 命令行程序开发教程


    #! /usr/bin/env node
    let yargs = require('yargs');
    let Server = require('../src/app.js')
    let argv = yargs.option('d', {
        alias: 'root',
        demand: false,
        description: '请配置监听静态文件目录',
        default: process.cwd(),
        type: 'string'
    }).option('o', {
        alias: 'host',
        demand: false,
        description: '请配置监听的主机',
        default: 'localhost',
        type: 'string'
    }).option('p', {
        alias: 'port',
        demand: false,
        description: '请配置监听的主机的端口号',
        default: 8080,
        type: 'number'
    }).usage('myselfsever -d / -p 8080 -o localhost')
        .example(
            'myselfsever -d / -p 9090 -o localhost', '在本机器的9090端口上监听客户端发来的请求'
        ).help('h').argv;
    
    
    let server = new Server();
    server.start(argv);
    

每天都进步一点;不要畏惧陌生的事物,每天都学习一点;

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,773评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,799评论 25 707
  • 交了房租那晚,我身体被抽空了,感觉广州所城市的灯光都被压低,楼下大排档的喧哗越发得刺耳。 微信零钱只剩下0.5,显...
    温粒阅读 233评论 1 1
  • 今夜,突然好想你 望着窗外,熙熙攘攘的人群中 能否看到那张熟悉的脸 今夜,思绪万千 好想握住那张给过我温暖的手 却...
    无法抗拒的容颜阅读 291评论 0 0