本文档介绍如何使用 Node.js 核心模块构建一个静态资源服务器,综合运用 http、fs、path、url、zlib 等模块,实现 MIME 类型映射、304 缓存协商、gzip 压缩传输等核心功能。
http、fs、path、url、zlib客户端请求
│
▼
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 │
└──────────────────────────┘HTTP 响应需要根据文件类型设置正确的 Content-Type,浏览器据此决定如何处理响应内容:
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': 'application/vnd.ms-fontobject',
'.ttf': 'application/font-sfnt'
}MIME 映射表同时也是后缀白名单:不在表中的扩展名一律返回 404,防止服务器暴露非公开资源。
HTTP 缓存协商通过 Last-Modified 和 If-Modified-Since 头部实现:
Last-Modified(文件的最后修改时间)If-Modified-Since(上次接收的修改时间)// 304 缓存有效期判断
// If-Modified-Since 是 HTTP 日期字符串,需解析为时间戳后再比较
const modified = req.headers['if-modified-since']
const expectedModified = new Date(fStat.mtime).getTime()
if (modified && new Date(modified).getTime() === expectedModified) {
res.statusCode = 304
res.setHeader('Content-Type', mimeType[ext])
res.setHeader('Cache-Control', 'max-age=3600')
res.setHeader('Last-Modified', new Date(expectedModified).toUTCString())
return
}也可使用
ETag替代Last-Modified进行缓存协商,ETag 基于文件内容生成哈希值,精度更高。
gzip 压缩通过 zlib.createGzip() 创建压缩 Transform 流,与 fs.createReadStream 形成管道:
// 流式压缩管道
fs.createReadStream(filePath)
.pipe(zlib.createGzip())
.pipe(res)管道模式的优势在于数据逐块处理,无需将整个文件读入内存再压缩,内存占用可控。
响应头需设置
Content-Encoding: gzip,浏览器据此选择正确的解压方式。
#!/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': 'application/vnd.ms-fontobject',
'.ttf': 'application/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)) {
res.writeHead(404)
return res.end()
}
// 3. 文件类型检查:确保路径指向文件而非目录
const fStat = fs.statSync(filePath)
if (!fStat.isFile()) {
res.writeHead(404)
return res.end()
}
// 4. 路径安全校验:确保请求文件位于 wwwroot 目录下,防止路径遍历攻击
if (!filePath.startsWith(wwwroot + path.sep)) {
res.writeHead(404)
return res.end()
}
// 5. 304 缓存协商:基于 If-Modified-Since
const modified = req.headers['if-modified-since']
const expectedModified = new Date(fStat.mtime).getTime()
if (modified && new Date(modified).getTime() === expectedModified) {
res.statusCode = 304
res.setHeader('Content-Type', mimeType[ext])
res.setHeader('Cache-Control', 'max-age=3600')
res.setHeader('Last-Modified', new Date(expectedModified).toUTCString())
return res.end()
}
// 6. 设置响应头(仅在客户端声明支持 gzip 时才压缩传输)
const acceptEncoding = req.headers['accept-encoding'] || ''
const useGzip = /\bgzip\b/.test(acceptEncoding)
res.statusCode = 200
res.setHeader('Content-Type', mimeType[ext])
res.setHeader('Cache-Control', 'max-age=3600')
if (useGzip) res.setHeader('Content-Encoding', 'gzip')
res.setHeader('Last-Modified', new Date(expectedModified).toUTCString())
// 7. 流式压缩传输:ReadStream → Gzip → Response
// 注意:不要给 createReadStream 传 encoding: 'utf8',
// 图片等二进制资源必须按 Buffer 读取,否则会被破坏
const stream = fs.createReadStream(filePath, { flags: 'r' })
stream.on('error', () => {
res.writeHead(404)
res.end()
})
const output = useGzip ? stream.pipe(zlib.createGzip()) : stream
output.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() |
Accept-Encoding 头部动态选择压缩算法(br > gzip > 无压缩)