MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js Express 静态文件服务的实现方法

2024-10-206.3k 阅读

一、Node.js Express 框架基础

1.1 Express 框架概述

Express 是基于 Node.js 平台的极简、灵活的 web 应用开发框架,它提供了一系列强大的特性来帮助开发者创建各种 Web 应用和 API。Express 框架的设计理念在于简洁和高效,它对 Node.js 原生的 HTTP 模块进行了封装,极大地简化了 Web 服务器的搭建过程。

例如,使用原生 Node.js 创建一个简单的 HTTP 服务器代码如下:

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello, World!');
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});

而使用 Express 框架实现相同功能的代码则简洁许多:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

从上述代码对比可以看出,Express 框架通过简洁的路由定义和便捷的响应处理方法,使得开发者可以更专注于业务逻辑的实现。

1.2 Express 中间件概念

中间件是 Express 框架中极为重要的概念。中间件函数可以访问请求对象(req)、响应对象(res)以及应用程序的请求 - 响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量来表示。

中间件函数可以执行以下任务:

  • 执行任何代码。
  • 修改请求和响应对象。
  • 终结请求 - 响应循环。
  • 调用堆栈中的下一个中间件函数。

如果当前中间件函数没有终结请求 - 响应循环,就必须调用 next() 将控制权传递给下一个中间件函数,否则请求将被挂起。

例如,一个简单的记录请求日志的中间件:

const express = require('express');
const app = express();

// 定义日志中间件
const logger = (req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
};

app.use(logger);

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

在上述代码中,logger 中间件在每个请求到达时会记录请求的方法和 URL,然后通过 next() 将控制权传递给下一个匹配的路由处理函数。

二、静态文件服务基础概念

2.1 什么是静态文件

静态文件是指在 Web 开发中不需要经过服务器动态处理就可以直接提供给客户端的文件,例如 HTML 文件、CSS 文件、JavaScript 文件、图片文件、字体文件等。这些文件的内容在服务器端是固定不变的,每次客户端请求时,服务器直接将文件内容返回给客户端。

例如,一个简单的 HTML 文件 index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Static File Example</title>
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <h1>Welcome to my page</h1>
  <script src="script.js"></script>
</body>

</html>

在这个 HTML 文件中,styles.cssscript.js 都是静态文件,它们分别用于定义页面的样式和交互逻辑。

2.2 为什么需要静态文件服务

在 Web 应用开发中,为了构建丰富的用户界面和提供流畅的用户体验,需要大量的静态文件。如果没有专门的静态文件服务,开发者需要手动编写代码来处理每个静态文件的请求,这将是非常繁琐且低效的。

通过设置静态文件服务,服务器可以高效地处理静态文件请求,提高响应速度,减轻服务器的负载。同时,合理配置静态文件服务还可以实现文件的缓存,进一步优化用户体验。例如,对于不经常变动的 CSS 和 JavaScript 文件,可以设置较长的缓存时间,这样客户端再次请求相同文件时,就可以直接从本地缓存中获取,而不需要再次从服务器下载。

三、Node.js Express 实现静态文件服务

3.1 使用 express.static 中间件

Express 框架提供了 express.static 中间件来快速实现静态文件服务。该中间件可以将指定目录下的文件暴露为静态资源,客户端可以直接通过 URL 访问这些文件。

3.1.1 基本使用方法

假设项目目录结构如下:

project/
├── public/
│   ├── index.html
│   ├── styles.css
│   └── script.js
└── app.js

app.js 文件中使用 express.static 中间件的代码如下:

const express = require('express');
const app = express();

// 将 public 目录设置为静态资源目录
app.use(express.static('public'));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

在上述代码中,通过 app.use(express.static('public'))public 目录设置为静态资源目录。此时,客户端可以通过 http://localhost:3000/index.html 访问 public 目录下的 index.html 文件,通过 http://localhost:3000/styles.css 访问 styles.css 文件,以此类推。

3.1.2 自定义访问路径前缀

默认情况下,express.static 中间件会将指定目录下的文件直接暴露在根路径下。但有时候,我们可能希望为静态文件设置一个特定的访问路径前缀,以避免与其他路由冲突或实现更好的目录结构管理。

例如,我们希望通过 /static 前缀来访问静态文件,代码如下:

const express = require('express');
const app = express();

// 将 public 目录设置为静态资源目录,并使用 /static 前缀
app.use('/static', express.static('public'));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

此时,客户端需要通过 http://localhost:3000/static/index.html 来访问 public 目录下的 index.html 文件。

3.1.3 多个静态资源目录

在实际项目中,可能会有多个目录存放静态文件,例如一个项目中既有公共的静态资源,又有特定模块的静态资源。Express 允许我们设置多个静态资源目录。

假设项目目录结构如下:

project/
├── public/
│   ├── common/
│   │   ├── styles.css
│   │   └── script.js
│   └── module1/
│       ├── index.html
│       └── module1.css
└── app.js

app.js 文件中设置多个静态资源目录的代码如下:

const express = require('express');
const app = express();

// 设置公共静态资源目录
app.use('/common', express.static('public/common'));
// 设置模块 1 的静态资源目录
app.use('/module1', express.static('public/module1'));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

这样,客户端可以通过 http://localhost:3000/common/styles.css 访问公共的样式文件,通过 http://localhost:3000/module1/index.html 访问模块 1 的页面文件。

3.2 静态文件服务的配置选项

express.static 中间件提供了一些配置选项,以满足不同场景下的需求。

3.2.1 dotfiles 选项

dotfiles 选项用于控制是否允许访问隐藏文件(文件名以点 . 开头的文件)。默认值为 'ignore',表示忽略隐藏文件的请求。

取值可以为:

  • 'ignore':忽略隐藏文件的请求,客户端无法访问隐藏文件。
  • 'allow':允许客户端访问隐藏文件。
  • 'deny':拒绝所有隐藏文件的请求,并返回 403 状态码。

例如,设置允许访问隐藏文件:

const express = require('express');
const app = express();

app.use(express.static('public', { dotfiles: 'allow' }));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

3.2.2 etag 选项

etag 选项用于控制是否启用 ETag(实体标签)。ETag 是一种缓存验证机制,它可以帮助服务器判断文件内容是否发生变化。默认值为 true,表示启用 ETag。

当启用 ETag 时,服务器会在响应头中添加 ETag 字段,客户端下次请求时会将该字段发送给服务器,服务器通过比较 ETag 值来判断文件是否需要重新发送。

例如,禁用 ETag:

const express = require('express');
const app = express();

app.use(express.static('public', { etag: false }));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

3.2.3 maxAge 选项

maxAge 选项用于设置静态文件在客户端的缓存时间(以毫秒为单位)。默认情况下,Express 会根据文件的修改时间来设置缓存,通过 maxAge 可以更精确地控制缓存时间。

例如,设置静态文件的缓存时间为 1 天(86400000 毫秒):

const express = require('express');
const app = express();

app.use(express.static('public', { maxAge: 86400000 }));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

这样设置后,客户端在 1 天内再次请求相同的静态文件时,将直接从本地缓存中获取,而不需要再次从服务器下载。

3.3 自定义静态文件服务逻辑

虽然 express.static 中间件已经能够满足大多数静态文件服务的需求,但在某些特殊情况下,我们可能需要自定义静态文件服务逻辑。

3.3.1 基于文件类型的处理

例如,我们希望对不同类型的文件设置不同的响应头。对于图片文件,我们希望设置 Cache - Control 头以启用缓存,对于 HTML 文件,我们希望设置特定的 Content - Security - Policy 头。

假设项目目录结构如下:

project/
├── public/
│   ├── index.html
│   └── logo.png
└── app.js

app.js 文件中的代码如下:

const express = require('express');
const app = express();
const path = require('path');
const fs = require('fs');

app.get('*', (req, res) => {
  const filePath = path.join(__dirname, 'public', req.path);
  fs.stat(filePath, (err, stats) => {
    if (err) {
      if (err.code === 'ENOENT') {
        res.status(404).send('File not found');
      } else {
        res.status(500).send('Server error');
      }
      return;
    }
    if (stats.isFile()) {
      const extname = path.extname(req.path);
      if (extname === '.png' || extname === '.jpg' || extname === '.jpeg') {
        res.set('Cache - Control','public, max - age = 31536000');
      } else if (extname === '.html') {
        res.set('Content - Security - Policy', "default - src'self'");
      }
      fs.createReadStream(filePath).pipe(res);
    } else {
      res.status(404).send('File not found');
    }
  });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

在上述代码中,通过 fs.stat 方法获取文件状态,判断文件是否存在以及是否为文件类型。根据文件的扩展名设置不同的响应头,然后通过 fs.createReadStream 读取文件内容并通过管道传输到响应中。

3.3.2 处理文件版本控制

在实际项目中,为了确保客户端获取到最新的静态文件,通常会采用文件版本控制的方法。一种常见的做法是在文件名中添加版本号或哈希值。

例如,假设我们有一个 styles.css 文件,在构建过程中,我们将其重命名为 styles.123456.css,其中 123456 可以是文件内容的哈希值。

在 Express 应用中,我们可以通过自定义中间件来处理这种版本控制。假设项目目录结构如下:

project/
├── public/
│   ├── styles.123456.css
│   └── index.html
└── app.js

index.html 文件中引用样式文件:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Versioned Static File</title>
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <h1>Welcome</h1>
</body>

</html>

app.js 文件中的代码如下:

const express = require('express');
const app = express();
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');

// 版本映射表,假设通过构建工具生成
const versionMap = {
 'styles.css':'styles.123456.css'
};

app.use((req, res, next) => {
  const requestedFile = req.path.split('/').pop();
  if (versionMap[requestedFile]) {
    req.url = req.url.replace(requestedFile, versionMap[requestedFile]);
  }
  next();
});

app.use(express.static('public'));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

在上述代码中,通过一个版本映射表 versionMap 将客户端请求的文件名映射到实际的带版本号的文件名。当请求到达时,中间件检查请求的文件名是否在映射表中,如果存在则替换请求的 URL,然后将请求传递给 express.static 中间件进行处理。

四、静态文件服务的优化

4.1 启用 Gzip 压缩

Gzip 压缩是一种常见的优化手段,它可以在不损失数据的前提下,对文件进行压缩,从而减少文件在网络传输中的大小,提高传输速度。

在 Express 应用中,可以使用 compression 中间件来启用 Gzip 压缩。首先,需要安装 compression 模块:

npm install compression

然后在 app.js 文件中使用该中间件:

const express = require('express');
const app = express();
const compression = require('compression');

app.use(compression());
app.use(express.static('public'));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

在上述代码中,通过 app.use(compression()) 启用了 Gzip 压缩。当客户端请求静态文件时,服务器会自动对文件进行压缩,并在响应头中添加 Content - Encoding: gzip 字段,客户端接收到压缩后的文件后会自动解压缩。

4.2 合理设置缓存

合理设置静态文件的缓存可以大大提高用户访问速度,减轻服务器负载。除了前面提到的通过 maxAge 选项设置缓存时间外,还可以结合 ETag 来更精确地控制缓存。

例如,对于不经常变动的 CSS 和 JavaScript 文件,可以设置较长的 maxAge,同时启用 ETag

const express = require('express');
const app = express();

app.use(express.static('public', { maxAge: 31536000, etag: true }));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

这样,客户端在很长一段时间内(一年)再次请求相同的 CSS 或 JavaScript 文件时,如果文件的 ETag 值没有变化,将直接从本地缓存中获取,而不需要再次从服务器下载。

4.3 采用 CDN(内容分发网络)

CDN 是一种分布式服务器网络,它根据用户的地理位置缓存和分发内容。通过将静态文件部署到 CDN,可以让用户从距离更近的服务器获取文件,从而提高加载速度。

在 Express 应用中,可以通过在 HTML 文件中引用 CDN 链接来使用 CDN。例如,对于 jQuery 库,可以在 index.html 文件中这样引用:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CDN Example</title>
  <script src="https://code.jquery.com/jquery - 3.6.0.min.js"></script>
</head>

<body>
  <h1>Using CDN</h1>
</body>

</html>

此外,也可以将自己的静态文件上传到 CDN 服务提供商(如七牛云、阿里云 OSS 等),并在 Express 应用中配置相应的 CDN 链接。这样,客户端请求静态文件时,将从 CDN 服务器获取文件,大大提高了文件的加载速度。

五、静态文件服务的安全考虑

5.1 防止目录遍历攻击

目录遍历攻击是一种常见的安全漏洞,攻击者通过构造特殊的 URL 来访问服务器上的敏感文件或目录。例如,通过 ../ 这样的字符串来跳出指定的静态资源目录,访问其他目录下的文件。

使用 express.static 中间件时,它会自动防止目录遍历攻击。但是,如果自定义静态文件服务逻辑,就需要特别注意。

例如,在前面自定义静态文件服务逻辑的代码中,通过 path.join(__dirname, 'public', req.path) 来获取文件路径,这样可以避免目录遍历攻击。因为 path.join 会正确处理路径,不会让 ../ 这样的字符串跳出指定的目录。

5.2 正确设置文件权限

在服务器上,确保静态文件所在目录和文件的权限设置正确。静态文件目录不应该具有可写权限,以防止攻击者上传恶意文件。

例如,在 Linux 系统中,可以使用以下命令设置目录权限:

chmod -R 755 public

上述命令将 public 目录及其所有子目录和文件的权限设置为所有者可读、可写、可执行,组用户和其他用户可读、可执行。这样可以保证静态文件的安全性。

5.3 内容安全策略(CSP)

内容安全策略(CSP)是一种用于增强网页安全性的机制,它可以限制网页可以加载的资源来源。在静态文件服务中,合理设置 CSP 可以防止跨站脚本攻击(XSS)等安全问题。

例如,在 Express 应用中,可以通过设置响应头来配置 CSP。假设我们只允许从当前域加载资源:

const express = require('express');
const app = express();

app.use((req, res, next) => {
  res.set('Content - Security - Policy', "default - src'self'");
  next();
});

app.use(express.static('public'));

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

在上述代码中,通过 res.set('Content - Security - Policy', "default - src'self'") 设置了 CSP,只允许从当前域加载资源,从而增强了应用的安全性。

通过以上对 Node.js Express 静态文件服务的实现方法、优化和安全考虑的介绍,开发者可以在 Web 应用开发中更好地设置和管理静态文件服务,提供高效、安全的用户体验。