You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Express判断静态资源请求所依据的请求头有哪些?

回答你的问题:Express静态资源中间件的判断逻辑与复刻方案

首先得澄清一个关键点:Express的express.static中间件并不是靠请求头来判断一个请求是否针对静态资源的——它的核心判断逻辑是:将请求的URL路径映射到你指定的静态资源目录后,检查对应的路径是否存在一个实际的文件(而非目录)。请求头更多是用来优化静态资源的处理行为(比如缓存、断点续传、压缩等),而非判断请求是否属于静态资源。

不过既然你想复刻这个逻辑,我会把中间件用到的请求头以及核心判断流程都拆解清楚:

一、Express静态资源中间件用到的关键请求头

虽然不是用来判断"是否是静态资源请求",但这些请求头会直接影响中间件的处理逻辑:

  • If-Modified-Since & If-None-Match:用于缓存校验。中间件会对比文件的Last-Modified(最后修改时间)和客户端发送的If-Modified-Since,或者文件的ETag(唯一标识)和If-None-Match,如果匹配,直接返回304 Not Modified,避免重复传输文件。
  • Range:处理断点续传。当客户端发送这个头时(比如Range: bytes=0-1023),中间件会返回206 Partial Content,只传输请求的文件片段。
  • Accept-Encoding:用于压缩支持。如果客户端支持gzip/brotli等压缩格式,且你开启了中间件的compress选项,中间件会返回预压缩或实时压缩后的文件,并设置Content-Encoding头。
  • Accept:配合extensions选项使用。如果请求路径没有扩展名,中间件会根据Accept头尝试添加对应的扩展名查找文件(比如请求/about,会尝试找/about.html或/about.htm)。

二、复刻静态资源中间件的核心步骤

要实现一个类似的中间件,你需要覆盖以下核心逻辑:

1. 路径映射与安全校验

  • 将请求的URL路径拼接成静态资源目录下的绝对路径,比如用path.resolve(rootDir, req.path)。
  • 必须做路径遍历防护:确保拼接后的路径始终在你指定的静态资源目录内,防止攻击者通过../访问服务器上的其他文件。

2. 文件存在性检查

  • 使用fs.stat或fs.promises.stat获取文件的状态信息,判断是否是一个实际的文件(stats.isFile())。如果是目录或者文件不存在,调用next()把请求交给后续中间件。

3. 缓存逻辑处理

  • 生成文件的ETag:通常可以用文件的大小、修改时间、inode(文件系统索引节点)组合生成唯一标识。
  • 对比客户端发送的If-None-Match和If-Modified-Since头,匹配的话返回304。

4. 响应头设置

  • 根据文件扩展名设置Content-Type(比如.html对应text/html)。
  • 设置Last-Modified和ETag头,方便客户端后续缓存。

5. 可选:断点续传与压缩

  • 解析Range头,计算要返回的字节范围,设置Content-Range头并返回206状态码。
  • 检查Accept-Encoding头,返回对应的压缩文件(如果有预压缩文件或者实时压缩)。

三、简化版复刻示例代码

下面是一个基础版的自定义静态资源中间件,覆盖了核心逻辑:

const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const crypto = require('crypto');

const app = express();

// 自定义静态资源中间件
function customStatic(rootDir) {
  return async (req, res, next) => {
    // 1. 映射请求路径到文件系统路径
    const resolvedRoot = path.resolve(rootDir);
    let filePath = path.resolve(resolvedRoot, req.path);

    // 路径遍历防护:确保文件在指定目录内
    if (!filePath.startsWith(resolvedRoot)) {
      return res.sendStatus(403);
    }

    try {
      const stats = await fs.stat(filePath);
      
      // 2. 检查是否是文件(不是目录)
      if (!stats.isFile()) {
        return next();
      }

      // 3. 处理缓存校验
      const mtime = stats.mtime.toUTCString();
      // 基于文件属性生成ETag
      const etag = crypto.createHash('md5')
        .update(`${stats.size}-${mtime}-${stats.ino}`)
        .digest('hex');

      // 匹配If-None-Match
      if (req.headers['if-none-match'] === etag) {
        return res.sendStatus(304);
      }
      // 匹配If-Modified-Since
      if (req.headers['if-modified-since'] === mtime) {
        return res.sendStatus(304);
      }

      // 4. 设置响应头
      res.set({
        'Last-Modified': mtime,
        'ETag': etag,
        'Content-Type': getContentType(path.extname(filePath))
      });

      // 5. 处理Range请求(简化版)
      if (req.headers.range) {
        const [startStr, endStr] = req.headers.range.replace('bytes=', '').split('-');
        const start = parseInt(startStr, 10) || 0;
        const end = parseInt(endStr, 10) || stats.size - 1;
        
        res.status(206);
        res.set('Content-Range', `bytes ${start}-${end}/${stats.size}`);
        
        const fileChunk = await fs.readFile(filePath, { start, end });
        return res.send(fileChunk);
      }

      // 6. 返回完整文件
      const fileContent = await fs.readFile(filePath);
      res.send(fileContent);
    } catch (err) {
      // 文件不存在,交给后续中间件
      if (err.code === 'ENOENT') {
        return next();
      }
      // 其他错误返回500
      res.sendStatus(500);
    }
  };
}

// 简单的MIME类型映射
function getContentType(ext) {
  const mimeMap = {
    '.html': 'text/html',
    '.css': 'text/css',
    '.js': 'application/javascript',
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.gif': 'image/gif',
    '.json': 'application/json'
  };
  return mimeMap[ext.toLowerCase()] || 'application/octet-stream';
}

// 使用自定义中间件
app.use('/', customStatic(path.resolve(__dirname + '/public')));

// 非静态资源请求处理
app.use('/', (req, res) => {
  res.send('This is not a static asset request');
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

这个示例实现了核心的静态资源处理逻辑,你可以根据需求扩展压缩、更完善的Range处理、文件扩展名自动匹配等功能。

内容的提问来源于stack exchange,提问作者user7898461

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:15:25