精读httpserver(一个简单的nodejs本地静态资源服务)源码

foreword(前言)

最近在看极客时间的“nodejs开发实战”,里面简单介绍了一个npm包——httpserver(https://github.com/bahamas10/node-httpserver#readme),它可以为我们搭建一个本地静态资源服务器。通过js,它是怎么做的呢,本着好奇的心态,我点进github查看了它的源码,发现只有100多行代码而且,所以有了这篇文章。

在这篇文章中,我想做的是将httpserver的源码进行拆解,并做简单的分析。

httpserver

1.概要

httpserver到底是个怎样的东西呢?

作者是这样来表述的:

command line HTTP server tool for serving up local files, similar to python -m SimpleHTTPServer.(httpserver是一个用于构建本地静态资源服务的命令行工具,它和“python -m SimpleHTTPServer”类似)

具体详细的用法这里就不做太多的介绍了,简单讲:

  • 它是一个命令行工具;
  • 通过在相应的静态资源目录下输入简单的命令(如:httpserver)即可构建一个静态资源服务,默认通过 http://0.0.0.0:8080/ 访问目录下的资源(默认为index.html或index.htm,否则显示目录结构);
  • 它有以下的优点:
    • 使用简单;
    • 可配置;
    • 安全;
    • 精简,所有代码只在一个文件中;
    • 可解析的,可以json的结构展示目录结构;
    • 标准的http:请求将会返回标准的Mime types和Caching headers;

2.npm包结构

在阅读源码之前,先了解一下整个npm包的结构:

/smf
---manifest.xml
httpserver.js
package.json

结构非常简单,全部的代码都写在httpserver.js中,manifest.xml文件在阅读完之后发现它是没用的。

3.依赖包

在阅读源码之前,我觉得首先了解下源码中有所依赖的包会比较好。

"dependencies": {
    "access-log": "~0.3.9",
    "static-route": "~0.1.2",
    "posix-getopt": "~1.2.0",
    "latest": "~0.2.0"
}

我们发现有这几个包,它们大致的作用如下:

  • access-log(获取访问日志)
  • static-route(安全的返回静态资源,具体之后再看)
  • posix-getopt(解析命令行参数)
  • latest(对依赖包进行检查更新)

4.源码

代码阅读起来也是比较简单的,主要可以分为2个部分:参数条件执行部分、服务构建部分。

参数条件执行部分中,通过getopt这个npm包对命令行中的参数进行解析与获取,并通过switch语句块基于不同的参数对配置项进行配置,代码如下:

var options = [
  'd(disable-index)',
  'h(help)',
  'H:(host)',
  'n(no-indexes)',
  'p:(port)',
  'u(updates)',
  'v(version)'
].join('');
var parser = new getopt.BasicParser(options, process.argv);

var opts = {
  disableindex: process.env.HTTPSERVER_NO_INDEX,
  host: process.env.HTTPSERVER_HOST || process.env.NODE_HOST,
  nodir: process.env.HTTPSERVER_NO_DIR_LISTING,
  port: process.env.HTTPSERVER_PORT || process.env.NODE_PORT,
};
var option;
while ((option = parser.getopt()) !== undefined) {
  switch (option.option) {
    case 'd': opts.disableindex = true; break;
    case 'h': console.log(usage()); process.exit(0);
    case 'H': opts.host = option.optarg; break;
    case 'n': opts.nodir = true; break;
    case 'p': opts.port = option.optarg; break;
    case 'u': // check for updates
      require('latest').checkupdate(package, function(ret, msg) {
        console.log(msg);
        process.exit(ret);
      });
      return;
    case 'v': console.log(package.version); process.exit(0);
    default: console.error(usage()); process.exit(1); break;
  }
}

源码的核心部分也就是我们的服务构建,最主要的是下面的3段代码:

var staticroute = require('static-route')(
  {
    autoindex: !opts.nodir,
    logger: function() {},
    tryfiles: opts.disableindex ? [] : ['index.html', 'index.htm']
  }
);

function onrequest(req, res) {
  accesslog(req, res);
  staticroute(req, res);
}

http.createServer(onrequest).listen(opts.port, opts.host, listening);

我们关注的点主要是onrequest中到底做了些什么,排除access-log的对请求日志的获取和打印,httpserver构建本地服务的核心其实在staticroute这个方法中,它调用了static-route这个npm包。

所以为了了解httpserver具体的工作原理,还得好好阅读下static-route的源码。

static-route

和httpserver是同一个作者,github地址为:https://github.com/bahamas10/node-static-route#readme。

A route for an http server to safely serve static files.(它是一个node服务的路由,为服务安全地返回静态文件资源)

具体的使用细节就不多说了,阅读完源码自然就知道了。

源码有200多行,我就不贴了,接下来直接拆解分析。

1.源码输出的是一个方法,方法中返回了一个staticroute的方法,也就是我们上面用到的那个staticroute方法,所以下面对这个方法进行拆解;

2.获取请求路径reqfile

var parsed = url.parse(req.url, true);
// copy the array
var tryfiles = opts.tryfiles.slice(0);

// decode everything, and then fight against dir traversal
var reqfile;
try {
    reqfile = path.normalize(decodeURIComponent(parsed.pathname));
} catch (e) {
    res.statusCode = 400;
    res.end();
    return;
}

// slice off opts.slice
if (opts.slice && reqfile.indexOf(opts.slice) === 0)
    reqfile = reqfile.substr(opts.slice.length);

3.判断请求的method,目前仅支持HEAD和GET,其他请求不支持则会返回501的状态码(501: Not Implemented,服务器不支持请求的功能,无法完成请求)

// unsupported methods
if (['HEAD', 'GET'].indexOf(req.method) === -1) {
    res.statusCode = 501;
    res.end();
    return;
}

4.拼接完整文件目录

var f = path.join((opts.dir || process.cwd()), reqfile);

5.接着执行一个tryfiles的方法,针对不存在的目录或文件、存在的目录、存在的文件进行不同的操作。这部分由于不好拆分,所以在代码中做了相应的注释:

function tryfile() {
    var file = path.join(f, tryfiles.pop());

    // the user wants some actual data
    fs.stat(file, function (err, stats) {
        if (err) {
            logger(err.message);
            if (tryfiles.length) // 不断从默认的访问文件文件名数组中取文件名,如果获取失败则重新从数组中取,直到成功,或直到失败
                return tryfile();

            res.statusCode = (err.code === 'ENOENT') ? 404 : 500; // 判断错误原因,如果错误码是ENOENT(个人比较喜欢理解成Error NO Entity,即no such file or directory),则将状态码设置成404,否则状态码设置成500(服务器内部错误,无法完成请求)
            res.end();
            return;
        }

        // 接着如果是能获取数据的,则判断是否是一个目录director(页面显示文件目录),或者是一个文件,则将文件读取出来
        if (stats.isDirectory()) {
            // directory
            // forbidden
            if (!opts.autoindex) { // 如果访问的是一个路径,并且httpserver配置了--no-dir-listing,则返回403,即禁止访问目录
                res.statusCode = 403;
                res.end();
                return;
            }

            // json stringify the dir
            statall(file, function (e, files) {
                if (e) {
                    logger(e.message);
                    res.statusCode = 500;
                    res.end();
                    return;
                }
                files = files.map(function (_file) { // 过滤列表中的目录
                    return _file.filename + (_file.directory ? '/' : '');
                });
                files.sort(function (a, b) { // 目录排序,优先展示文件,其次按照大小排序
                    a = a.toLowerCase();
                    b = b.toLowerCase();
                    var adir = a.indexOf('/') > -1;
                    var bdir = b.indexOf('/') > -1;
                    if (adir && !bdir)
                        return -1;
                    else if (bdir && !adir)
                        return 1;
                    return a < b ? -1 : 1;
                });
                if (hap(parsed.query, 'json')) { // 如果请求中加了参数json,则将files转成json格式并输出
                    res.setHeader('Content-Type', 'application/json; charset=utf-8');
                    res.write(JSON.stringify(files));
                } else { // 否则以html的形式输出
                    res.setHeader('Content-Type', 'text/html; charset=utf-8');
                    writehtml(res, parsed.pathname, files);
                }
                res.end();
            });
        } else { // 如果访问的是一个文件则,直接返回文件流
            streamfile(file, stats, req, res);
        }
    });
}

6.上面代码中针对不同的情况调用了3个不同的方法:

  • streamfile:配置必要的响应头信息,并以文件流的形式输出;
  • statall:用于获取目录信息,以便在请求某个目录时将目录以json或者html的形式显示;
  • writehtml:将目录以html的形式返回;

图例

最后,整了一份简单的图例来总结httpserver中涉及到的某些关系:

在这里插入图片描述

last(最后)
非常感谢您能阅读完这篇文章,您的阅读是我不断前进的动力。

对于上面所述,有什么新的观点或发现有什么错误,希望您能指出。

最后,附上个人常逛的社交平台:
知乎:https://www.zhihu.com/people/bi-an-yao-91/activities
csdn:https://blog.csdn.net/YaoDeBiAn
github: https://github.com/yaodebian

我的邮箱:2801540120@qq.com or yaodebian@gmail.com

个人目前能力有限,并没有自主构建一个社区的能力,如有任何问题或想法与我沟通,请通过上述某个平台联系我,谢谢!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值