2016年更新
使用 Express 和不使用 Express 的示例实际上工作
这个问题已有 5 年多了,但是每个答案都有一些问题.
TL;DR
向下滚动查看使用以下方式提供图像的示例:
express.static
express
connect
http
net
所有示例也位于 GitHub 上:https://github.com/rsp/node-static-http-servers https://github.com/rsp/node-static-http-servers
测试结果可在 Travis 上获取:https://travis-ci.org/rsp/node-static-http-servers https://travis-ci.org/rsp/node-static-http-servers
介绍
自从这个问题被提出五年多以来,只有一个正确答案 https://stackoverflow.com/a/5823807/613198 by 亨利将军但即使这个答案与代码没有问题,它似乎有一些问题接待。有评论称它“除了如何依靠别人来完成工作之外,没有解释太多”事实上有多少人对此评论投了赞成票,这清楚地表明很多事情需要澄清。
首先,“如何使用 Node.js 提供图像”的一个很好的答案不是实现从头开始静态文件服务器并且做得很糟糕。一个好的答案是使用模块喜欢表达正确地完成工作.
回答说使用 Express 的评论“除了如何依靠别人来完成工作之外,没有解释太多”应该指出的是,使用http
module already依靠别人来完成工作。如果有人不想依靠任何人来完成工作那么at least应该使用原始 TCP 套接字 - 我在下面的示例之一中就是这样做的。
一个更严重的问题是这里所有的答案都使用http
模块是broken。他们介绍竞争条件, 不安全的路径解析这将导致路径遍历漏洞, 阻塞 I/O那将完全无法满足任何并发请求以及其他微妙的问题 - 它们作为问题所询问内容的示例完全被打破,但它们已经使用了由http
模块而不是使用 TCP 套接字,因此他们甚至不像他们声称的那样从头开始做所有事情。
如果问题是“如何从头开始实现静态文件服务器,作为学习练习”,那么无论如何应该发布如何做到这一点的答案 - 但即便如此,我们也应该期望它们至少是correct。此外,假设想要提供图像的人可能希望在将来提供更多图像,这并不是没有道理的,因此有人可能会说,编写一个只能为具有硬编码路径的单个文件提供服务的特定自定义静态文件服务器是有点短视。似乎很难想象任何搜索如何提供图像的答案的人都会满足于仅提供单个图像的解决方案,而不是提供任何图像的通用解决方案。
简而言之,问题是如何提供图像,答案是使用适当的模块在安全、高性能且可靠的方式那是可读、可维护且面向未来在使用时最佳实践专业的 Node 开发。但我同意,对这样的答案的一个很好的补充是展示一种手动实现相同功能的方法,但遗憾的是到目前为止,每一次尝试都失败了。这就是我写一些新例子的原因。
在简短的介绍之后,下面是我在 5 个不同的抽象级别上完成这项工作的五个示例。
最低功能
每个示例都提供来自public
目录并支持以下最低功能:
- 最常见文件的 MIME 类型
- 提供 HTML、JS、CSS、纯文本和图像
- serves
index.html
作为默认目录索引
- 响应丢失文件的错误代码
- 无路径遍历漏洞
- 读取文件时没有竞争条件
我在 Node 版本 4、5、6 和 7 上测试了每个版本。
express.static
该版本使用express.static https://expressjs.com/en/starter/static-files.html的内置中间件express https://expressjs.com/ module.
该示例功能最多,代码量最少。
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 https://expressjs.com/模块但没有express.static
中间件。提供静态文件是使用流作为单个路由处理程序实现的。
该示例具有简单的路径遍历对策,并支持有限的大多数路径遍历对策。common MIME types https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types.
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 http://senchalabs.github.com/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 https://nodejs.org/api/http.html#http_http模块是 Node.js 中 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 https://nodejs.org/api/net.html#net_net模块是 Node.js 中 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
and net
:
- https://github.com/rsp/node-static-http-servers https://github.com/rsp/node-static-http-servers
其他项目仅使用express.static
:
- https://github.com/rsp/node-express-static-example https://github.com/rsp/node-express-static-example
Tests
测试结果可在 Travis 上获取:
- https://travis-ci.org/rsp/node-static-http-servers https://travis-ci.org/rsp/node-static-http-servers
一切都在 Node 版本 4、5、6 和 7 上进行了测试。
See also
其他相关回答:
- 重定向 Javascript 时无法从同一目录加载资源 https://stackoverflow.com/questions/38441863/failed-to-load-resource-from-same-directory-when-redirecting-javascript/38442747#38442747
- onload js 调用不适用于节点 https://stackoverflow.com/questions/38587286/onload-js-call-not-working-with-node/38587729#38587729
- 通过快递将整个文件夹内容发送给客户端 https://stackoverflow.com/questions/40509666/sending-whole-folder-content-to-client-with-express/40510339#40510339
- 在服务器 JS 上加载部分失败 https://stackoverflow.com/questions/40722476/loading-partials-fails-on-the-server-js/40722594#40722594
- Node JS 不提供静态图像 https://stackoverflow.com/questions/40837359/node-js-not-serving-the-static-image/40839534#40839534