Skip to content

使用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.htmlmy-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。