{T}

静态资源服务器

本文档介绍如何使用 Node.js 核心模块构建一个静态资源服务器,综合运用 httpfspathurlzlib 等模块,实现 MIME 类型映射、304 缓存协商、gzip 压缩传输等核心功能。

项目概述

功能特性

  • MIME 类型映射:根据文件扩展名自动设置 Content-Type
  • 路径安全校验:防止路径遍历攻击,确保请求文件在根目录内
  • 304 缓存协商:基于 Last-Modified / If-Modified-Since 实现 HTTP 缓存
  • gzip 压缩传输:流式压缩响应,减少传输体积
  • 参数合法性校验:后缀白名单 + 文件存在性 + 类型检查

技术栈

  • 运行环境:Node.js(纯核心模块,无需第三方依赖)
  • 模块httpfspathurlzlib

处理流程

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-ModifiedIf-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解析请求 URLurl.parse(req.url) 获取 pathname
path构建文件路径、获取扩展名path.join()path.extname()
fs文件操作fs.existsSync()fs.statSync()fs.createReadStream()
zlibgzip 压缩zlib.createGzip()

扩展方向

  1. 支持 Brotli 压缩:根据 Accept-Encoding 头部动态选择压缩算法(br > gzip > 无压缩)
  2. 目录自动索引:当请求路径为目录时,自动生成文件列表 HTML
  3. Range 请求支持:实现大文件的分段传输(206 Partial Content)
  4. ETag 缓存协商:基于文件内容哈希生成 ETag,替代或补充 Last-Modified
  5. CORS 支持:添加跨域资源共享头部
  6. 日志记录:记录请求路径、状态码、响应时间等信息