2016 年更新
使用 Express 和不使用 Express 的示例实际工作
这个问题已经超过 5 年了,但是每个答案都有一些问题。
TL;DR
向下滚动查看示例以提供图片:
express.static
express
connect
http
net
所有示例也在 GitHub 上:https://github.com/rsp/node-static-http-servers
测试结果可在 Travis 上获得:https://travis-ci.org/rsp/node-static-http-servers
简介
自从提出这个问题 5 年多之后,generalhenry 只有 one correct answer,但即使该答案的代码没有问题,接收似乎也有一些问题。有人评论说它“除了如何依靠别人来完成工作之外没有解释太多”,有多少人投票赞成这条评论清楚地表明很多事情需要澄清。
首先,“如何使用 Node.js 提供图像”的一个很好的答案是没有实现从头开始的静态文件服务器并且做得不好。一个好的答案是使用像 Express 这样的模块正确地完成工作。
回答 cmets 说使用 Express “除了如何依赖别人来完成工作之外并没有解释太多” 应该注意的是,使用 http 模块 已经依赖其他人来完成工作。如果有人不想依赖任何人来完成工作,那么应该至少使用原始 TCP 套接字——我在下面的一个示例中就是这样做的。
更严重的问题是这里所有使用http 模块的答案都损坏了。它们引入了竞争条件、不安全的路径解析会导致路径遍历漏洞、阻塞I/O会导致完全无法为任何并发请求提供服务以及其他微妙的问题 - 作为问题所问问题的示例,它们完全被破坏了,但它们已经使用了 http 模块提供的抽象而不是使用 TCP 套接字,因此他们甚至不会像他们声称的那样从头开始做所有事情。
如果问题是“如何从头开始实现静态文件服务器,作为学习练习”,那么无论如何都应该发布如何做到这一点的答案 - 但即使这样,我们也应该期望它们至少是正确的。此外,假设想要提供图像的人将来可能希望提供更多图像并不是不合理的,因此有人可能会争辩说,编写一个特定的自定义静态文件服务器,它只能提供一个具有硬编码路径的单个文件是有些短视。似乎很难想象,任何搜索如何提供图像的答案的人都会满足于只提供单个图像的解决方案,而不是提供任何图像的通用解决方案。
简而言之,问题是如何提供图像,而对此的答案是使用适当的模块以安全、预先和可靠的方式可读,在使用专业节点开发的最佳实践的同时可维护且面向未来。但我同意,对这样的答案的一个很好的补充将是展示一种手动实现相同功能的方法,但遗憾的是,到目前为止,每一次尝试都失败了。这就是为什么我写了一些新的例子。
在这个简短的介绍之后,下面是我在 5 个不同抽象级别上完成这项工作的五个示例。
最低限度的功能
每个示例都提供来自public 目录的文件,并支持以下功能的最低限度:
- 最常见文件的 MIME 类型
- 提供 HTML、JS、CSS、纯文本和图像
- 将
index.html 用作默认目录索引
- 以错误代码响应丢失文件
- 没有路径遍历漏洞
- 读取文件时没有竞争条件
我在 Node 版本 4、5、6 和 7 上测试了每个版本。
express.static
此版本使用express模块的express.static内置中间件。
这个例子功能最多,代码最少。
var path = require('path');
var express = require('express');
var app = express();
var dir = path.join(__dirname, 'public');
app.use(express.static(dir));
app.listen(3000, function () {
console.log('Listening on http://localhost:3000/');
});
express
此版本使用express 模块但没有express.static 中间件。提供静态文件是作为使用流的单个路由处理程序实现的。
此示例具有简单的路径遍历对策,并支持有限的一组最常见的 MIME 类型。
var path = require('path');
var express = require('express');
var app = express();
var fs = require('fs');
var dir = path.join(__dirname, 'public');
var mime = {
html: 'text/html',
txt: 'text/plain',
css: 'text/css',
gif: 'image/gif',
jpg: 'image/jpeg',
png: 'image/png',
svg: 'image/svg+xml',
js: 'application/javascript'
};
app.get('*', function (req, res) {
var file = path.join(dir, req.path.replace(/\/$/, '/index.html'));
if (file.indexOf(dir + path.sep) !== 0) {
return res.status(403).end('Forbidden');
}
var type = mime[path.extname(file).slice(1)] || 'text/plain';
var s = fs.createReadStream(file);
s.on('open', function () {
res.set('Content-Type', type);
s.pipe(res);
});
s.on('error', function () {
res.set('Content-Type', 'text/plain');
res.status(404).end('Not found');
});
});
app.listen(3000, function () {
console.log('Listening on http://localhost:3000/');
});
connect
此版本使用connect 模块,它比express 低一级抽象。
此示例具有与 express 版本类似的功能,但使用的 API 级别稍低。
var path = require('path');
var connect = require('connect');
var app = connect();
var fs = require('fs');
var dir = path.join(__dirname, 'public');
var mime = {
html: 'text/html',
txt: 'text/plain',
css: 'text/css',
gif: 'image/gif',
jpg: 'image/jpeg',
png: 'image/png',
svg: 'image/svg+xml',
js: 'application/javascript'
};
app.use(function (req, res) {
var reqpath = req.url.toString().split('?')[0];
if (req.method !== 'GET') {
res.statusCode = 501;
res.setHeader('Content-Type', 'text/plain');
return res.end('Method not implemented');
}
var file = path.join(dir, reqpath.replace(/\/$/, '/index.html'));
if (file.indexOf(dir + path.sep) !== 0) {
res.statusCode = 403;
res.setHeader('Content-Type', 'text/plain');
return res.end('Forbidden');
}
var type = mime[path.extname(file).slice(1)] || 'text/plain';
var s = fs.createReadStream(file);
s.on('open', function () {
res.setHeader('Content-Type', type);
s.pipe(res);
});
s.on('error', function () {
res.setHeader('Content-Type', 'text/plain');
res.statusCode = 404;
res.end('Not found');
});
});
app.listen(3000, function () {
console.log('Listening on http://localhost:3000/');
});
http
此版本使用http 模块,它是 Node 中 HTTP 的最低级别 API。
此示例具有与 connect 版本类似的功能,但使用了更多低级 API。
var path = require('path');
var http = require('http');
var fs = require('fs');
var dir = path.join(__dirname, 'public');
var mime = {
html: 'text/html',
txt: 'text/plain',
css: 'text/css',
gif: 'image/gif',
jpg: 'image/jpeg',
png: 'image/png',
svg: 'image/svg+xml',
js: 'application/javascript'
};
var server = http.createServer(function (req, res) {
var reqpath = req.url.toString().split('?')[0];
if (req.method !== 'GET') {
res.statusCode = 501;
res.setHeader('Content-Type', 'text/plain');
return res.end('Method not implemented');
}
var file = path.join(dir, reqpath.replace(/\/$/, '/index.html'));
if (file.indexOf(dir + path.sep) !== 0) {
res.statusCode = 403;
res.setHeader('Content-Type', 'text/plain');
return res.end('Forbidden');
}
var type = mime[path.extname(file).slice(1)] || 'text/plain';
var s = fs.createReadStream(file);
s.on('open', function () {
res.setHeader('Content-Type', type);
s.pipe(res);
});
s.on('error', function () {
res.setHeader('Content-Type', 'text/plain');
res.statusCode = 404;
res.end('Not found');
});
});
server.listen(3000, function () {
console.log('Listening on http://localhost:3000/');
});
net
此版本使用net 模块,它是 Node 中 TCP 套接字的最低级别 API。
此示例具有http 版本的一些功能,但最小且不完整的 HTTP 协议是从头开始实现的。由于它不支持分块编码,因此它会先将文件加载到内存中,然后再为它们提供服务以在发送响应之前知道其大小,因为统计文件然后加载会引入竞争条件。
var path = require('path');
var net = require('net');
var fs = require('fs');
var dir = path.join(__dirname, 'public');
var mime = {
html: 'text/html',
txt: 'text/plain',
css: 'text/css',
gif: 'image/gif',
jpg: 'image/jpeg',
png: 'image/png',
svg: 'image/svg+xml',
js: 'application/javascript'
};
var server = net.createServer(function (con) {
var input = '';
con.on('data', function (data) {
input += data;
if (input.match(/\n\r?\n\r?/)) {
var line = input.split(/\n/)[0].split(' ');
var method = line[0], url = line[1], pro = line[2];
var reqpath = url.toString().split('?')[0];
if (method !== 'GET') {
var body = 'Method not implemented';
con.write('HTTP/1.1 501 Not Implemented\n');
con.write('Content-Type: text/plain\n');
con.write('Content-Length: '+body.length+'\n\n');
con.write(body);
con.destroy();
return;
}
var file = path.join(dir, reqpath.replace(/\/$/, '/index.html'));
if (file.indexOf(dir + path.sep) !== 0) {
var body = 'Forbidden';
con.write('HTTP/1.1 403 Forbidden\n');
con.write('Content-Type: text/plain\n');
con.write('Content-Length: '+body.length+'\n\n');
con.write(body);
con.destroy();
return;
}
var type = mime[path.extname(file).slice(1)] || 'text/plain';
var s = fs.readFile(file, function (err, data) {
if (err) {
var body = 'Not Found';
con.write('HTTP/1.1 404 Not Found\n');
con.write('Content-Type: text/plain\n');
con.write('Content-Length: '+body.length+'\n\n');
con.write(body);
con.destroy();
} else {
con.write('HTTP/1.1 200 OK\n');
con.write('Content-Type: '+type+'\n');
con.write('Content-Length: '+data.byteLength+'\n\n');
con.write(data);
con.destroy();
}
});
}
});
});
server.listen(3000, function () {
console.log('Listening on http://localhost:3000/');
});
下载示例
我在 GitHub 上发布了所有示例并提供了更多解释。
express.static、express、connect、http 和 net 的示例:
仅使用express.static的其他项目:
测试
测试结果可在 Travis 上获得:
一切都在 Node 版本 4、5、6 和 7 上进行了测试。
另见
其他相关答案: