使用Node.js创建静态文件服务器
一个简单的静态文件服务器
你可以创建的最简单的初级后端项目之一就是静态文件服务器。在其最简单的形式中,静态文件服务器将监听请求,并尝试将请求的URL与本地文件系统中的文件进行匹配。下面是一个最简单的示例:
import { readFile } from 'fs';
import { createServer } from 'http';
createServer((req, res) => {
readFile(__dirname + req.url, (err, data) => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('404: 文件未找到');
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(data);
}
});
}).listen(8000);
在这个代码示例中,我们使用fs
模块来读取__dirname + req.url
处的文件。如果文件不存在,我们将返回404
错误。否则,我们将返回文件内容。http
模块用于创建监听端口8000
的服务器。
理论上,我们可以在这里停下来,拥有一个非常基本的静态文件服务器。然而,还有一些考虑因素可以被纳入考虑。让我们逐个探讨它们,并看看如何解决它们。
模块化
首先,我们不一定希望从与我们的Node.js服务器相同的目录中提供文件。为了解决这个问题,我们需要更改fs.readFile()
在其中查找文件的目录。为了实现这一点,我们可以指定一个目录来提供文件,并使用path
模块从该目录解析文件。这样,我们还可以更好地处理不同的操作系统和环境。
下面是使用path
模块解析文件路径的简短代码片段:
import { readFile } from 'fs';
import { join } from 'path';
const directoryName = './public';
const requestUrl = 'index.html';
const filePath = join(directoryName, requestUrl);
readFile(filePath, (err, data) => {
// ...
});
安全性
我们接下来关注的是安全性。显然,我们不希望未经授权的用户在我们的机器上窥探。目前,可以通过访问指定根目录之外的文件(例如 GET /../../../
)来实现。为了解决这个问题,我们可以再次使用 path
模块来检查请求的文件是否在根目录内。
import { join, normalize, resolve } from 'path';
const directoryName = './public';
const root = normalize(resolve(directoryName));
const requestUrl = 'index.html';
const filePath = join(root, fileName);
const isPathUnderRoot = normalize(resolve(filePath)).startsWith(root);
同样,我们可以通过检查文件类型来确保用户无法访问敏感文件。为了实现这一点,我们可以指定一个支持的文件类型的数组或对象,并使用path
模块再次检查文件的扩展名。
import { extname } from 'path';
const types = ['html', 'css', 'js', 'json'];
const requestUrl = 'index.html';
const extension = extname(requestUrl).slice(1);
const isTypeSupported = types.includes(extension);
省略HTML扩展名
大多数网站的一个特点是在请求HTML页面时可以省略文件扩展名的URL。这是用户期望的一个小的生活质量改进,我们很希望将其添加到我们的静态文件服务器中。
这就是事情变得有点棘手的地方。为了提供这个功能,我们需要检查缺少的扩展名并查找适当的HTML文件。但请记住,对于诸如/my-page
这样的URL,有两个可能的匹配项。这个路径可以匹配/my-page.html
或my-page/index.html
。为了处理这个问题,我们将优先考虑其中一个。在我们的例子中,我们将优先考虑/my-page.html
而不是my-page/index.html
,但是很容易将它们交换。
为了实现这一点,我们可以使用fs
模块来检查它们中的一个是否存在,并适当处理。还需要为根URL(/
)添加一个特殊情况,将其匹配到index.html
文件。
import { accessSync, constants } from 'fs';
import { join, normalize, resolve, extname } from 'path';
```js
const directoryName = './public';
const root = normalize(resolve(directoryName));
const extension = extname(req.url).slice(1);
let fileName = requestUrl;
if (requestUrl === '/') fileName = 'index.html';
else if (!extension) {
try {
accessSync(join(root, requestUrl + '.html'), constants.F_OK);
fileName = requestUrl + '.html';
} catch (e) {
fileName = join(requestUrl, 'index.html');
}
}
最后的调整
在实现了上述所有内容之后,我们可以将所有内容放在一起,创建一个具有所需功能的静态文件服务器。我会添加一些最后的调整,例如将请求记录到控制台并处理更多的文件类型,以下是最终的代码:
import { readFile, accessSync, constants } from 'fs';
import { createServer } from 'http';
import { join, normalize, resolve, extname } from 'path';
const port = 8000;
const directoryName = './public';
const types = {
html: 'text/html',
css: 'text/css',
js: 'application/javascript',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
json: 'application/json',
xml: 'application/xml',
};
const root = normalize(resolve(directoryName));
const server = createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
const extension = extname(req.url).slice(1);
const type = extension ? types[extension] : types.html;
const supportedExtension = Boolean(type);
if (!supportedExtension) {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('404: 文件未找到');
return;
}
let fileName = req.url;
if (req.url === '/') fileName = 'index.html';
else if (!extension) {
try {
accessSync(join(root, req.url + '.html'), constants.F_OK);
fileName = req.url + '.html';
} catch (e) {
fileName = join(req.url, 'index.html');
}
}
const filePath = join(root, fileName);
const isPathUnderRoot = normalize(resolve(filePath)).startsWith(root);
if (!isPathUnderRoot) {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('404: 文件未找到');
return;
}
readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('404: 文件未找到');
} else {
res.writeHead(200, { 'Content-Type': type });
res.end(data);
}
});
server.listen(port, () => {
console.log(`服务器正在监听端口 ${port}`);
});
还不错吧?仅用70行代码,我们就成功创建了一个相当不错的静态文件服务器,而且只使用了核心的Node.js API。