静态资源服务器
本文档介绍如何使用 Node.js 核心模块构建一个静态资源服务器,综合运用 http、fs、path、url、zlib 等模块,实现 MIME 类型映射、304 缓存协商、gzip 压缩传输等核心功能。
项目概述
功能特性
- MIME 类型映射:根据文件扩展名自动设置 Content-Type
- 路径安全校验:防止路径遍历攻击,确保请求文件在根目录内
- 304 缓存协商:基于 Last-Modified / If-Modified-Since 实现 HTTP 缓存
- gzip 压缩传输:流式压缩响应,减少传输体积
- 参数合法性校验:后缀白名单 + 文件存在性 + 类型检查
技术栈
- 运行环境:Node.js(纯核心模块,无需第三方依赖)
- 模块:
http、fs、path、url、zlib
处理流程
code
客户端请求
│
▼
url.parse 解析 pathname
│
▼
path.extname 获取扩展名
│
▼
┌──────────────────────────┐
│ 1. MIME 白名单校验 │──不通过──► 404
└──────────────────────────┘
│ 通过
▼
┌──────────────────────────┐
│ 2. 文件存在性检查 │──不存在──► 404
└──────────────────────────┘
│ 存在
▼
┌──────────────────────────┐
│ 3. 文件类型检查 │──非文件──► 404
└──────────────────────────┘
│ 是文件
▼
┌──────────────────────────┐
│ 4. 路径安全校验 │──越界──► 404
└──────────────────────────┘
│ 安全
▼
┌──────────────────────────┐
│ 5. 304 缓存协商 │──未变化──► 304
└──────────────────────────┘
│ 已变化
▼
┌──────────────────────────┐
│ 6. 设置响应头 │
│ - Content-Type │
│ - Cache-Control │
│ - Content-Encoding │
│ - Last-Modified │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ 7. ReadStream → Gzip → Response │
└──────────────────────────┘MIME 类型映射
HTTP 响应需要根据文件类型设置正确的 Content-Type,浏览器据此决定如何处理响应内容:
javascript
const mimeType = {
'.ico': 'image/x-icon',
'.md': 'text/plain',
'.html': 'text/html',
'.js': 'application/javascript',
'.json': 'application/json',
'.css': 'text/css',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.wav': 'audio/wav',
'.mp3': 'audio/mpeg',
'.svg': 'image/svg+xml',
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.eot': 'appliaction/vnd.ms-fontobject',
'.ttf': 'aplication/font-sfnt'
}MIME 映射表同时也是后缀白名单:不在表中的扩展名一律返回 404,防止服务器暴露非公开资源。
304 缓存协商
HTTP 缓存协商通过 Last-Modified 和 If-Modified-Since 头部实现:
- 服务器在首次响应时设置
Last-Modified(文件的最后修改时间) - 客户端后续请求携带
If-Modified-Since(上次接收的修改时间) - 服务器比对时间戳:若文件未变化,返回 304 状态码(无响应体),客户端使用本地缓存
- 若文件已变化,返回 200 状态码和最新内容
javascript
// 304 缓存有效期判断
const modified = req.headers['if-modified-since']
const expectedModified = new Date(fStat.mtime).getTime()
if (modified && modified == expectedModified) {
res.statusCode = 304
res.setHeader('Content-Type', mimeType[ext])
res.setHeader('Cache-Control', 'max-age=3600')
res.setHeader('Last-Modified', new Date(expectedModified).toGMTString())
return
}也可使用
ETag替代Last-Modified进行缓存协商,ETag 基于文件内容生成哈希值,精度更高。
gzip 压缩传输
gzip 压缩通过 zlib.createGzip() 创建压缩 Transform 流,与 fs.createReadStream 形成管道:
javascript
// 流式压缩管道
fs.createReadStream(filePath)
.pipe(zlib.createGzip())
.pipe(res)管道模式的优势在于数据逐块处理,无需将整个文件读入内存再压缩,内存占用可控。
响应头需设置
Content-Encoding: gzip,浏览器据此选择正确的解压方式。
完整实现
javascript
#!/usr/bin/env node
const fs = require('fs')
const url = require('url')
const http = require('http')
const path = require('path')
const zlib = require('zlib')
const wwwroot = '/home/admin/wwwroot'
const mimeType = {
'.ico': 'image/x-icon',
'.md': 'text/plain',
'.html': 'text/html',
'.js': 'application/javascript',
'.json': 'application/json',
'.css': 'text/css',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.wav': 'audio/wav',
'.mp3': 'audio/mpeg',
'.svg': 'image/svg+xml',
'.pdf': 'application/pdf',
'.doc': 'application/msword',
'.eot': 'appliaction/vnd.ms-fontobject',
'.ttf': 'aplication/font-sfnt'
}
const server = http.createServer((req, res) => {
const { pathname } = url.parse(req.url)
const filePath = path.join(wwwroot, pathname)
const ext = path.extname(pathname)
// 1. 后缀白名单校验:非允许后缀的资源不予返回
if (!mimeType[ext]) {
res.writeHead(404)
return res.end()
}
// 2. 文件存在性检查
if (!fs.existsSync(filePath)) {
return (res.statusCode = 404)
}
// 3. 文件类型检查:确保路径指向文件而非目录
const fStat = fs.statSync(filePath)
if (!fStat.isFile()) {
return (res.statusCode = 404)
}
// 4. 路径安全校验:确保请求文件位于 wwwroot 目录下,防止路径遍历攻击
if (!filePath.startsWith(wwwroot)) {
return (res.statusCode = 404)
}
// 5. 304 缓存协商:基于 If-Modified-Since
const modified = req.headers['if-modified-since']
const expectedModified = new Date(fStat.mtime).getTime()
if (modified && modified == expectedModified) {
res.statusCode = 304
res.setHeader('Content-Type', mimeType[ext])
res.setHeader('Cache-Control', 'max-age=3600')
res.setHeader('Last-Modified', new Date(expectedModified).toGMTString())
return
}
// 6. 设置响应头
res.statusCode = 200
res.setHeader('Content-Type', mimeType[ext])
res.setHeader('Cache-Control', 'max-age=3600')
res.setHeader('Content-Encoding', 'gzip')
res.setHeader('Last-Modified', new Date(expectedModified).toGMTString())
// 7. 流式压缩传输:ReadStream → Gzip → Response
const stream = fs.createReadStream(filePath, {
flags: 'r', encoding: 'utf8'
})
stream.on('error', () => {
res.writeHead(404)
res.end()
})
stream.pipe(zlib.createGzip()).pipe(res)
})
server.on('error', error => console.log(error))
server.listen(4000, '127.0.0.1')模块协作关系
本案例中各核心模块的职责如下:
| 模块 | 职责 | 具体用法 |
|---|---|---|
http | 创建 HTTP 服务器 | http.createServer() |
url | 解析请求 URL | url.parse(req.url) 获取 pathname |
path | 构建文件路径、获取扩展名 | path.join()、path.extname() |
fs | 文件操作 | fs.existsSync()、fs.statSync()、fs.createReadStream() |
zlib | gzip 压缩 | zlib.createGzip() |
扩展方向
- 支持 Brotli 压缩:根据
Accept-Encoding头部动态选择压缩算法(br>gzip> 无压缩) - 目录自动索引:当请求路径为目录时,自动生成文件列表 HTML
- Range 请求支持:实现大文件的分段传输(206 Partial Content)
- ETag 缓存协商:基于文件内容哈希生成 ETag,替代或补充 Last-Modified
- CORS 支持:添加跨域资源共享头部
- 日志记录:记录请求路径、状态码、响应时间等信息