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
相关产品推荐
相关产品推荐

