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