@eggjs/koa-static-cache 版本演进与静态缓存中间件实战解析
【免费下载链接】egg🥚🥚🥚🥚 Born to build better enterprise frameworks and apps with Node.js & Koa. https://307.run/eggcode项目地址: https://gitcode.com/gh_mirrors/eg/egg
导读
@eggjs/koa-static-cache是 Egg 框架体系内置的 Koa 静态文件缓存中间件,它以初始化即缓存、可选内存驻留、MD5 ETag、原生 gzip 支持等特性区别于同类静态服务中间件。本文以该包的 CHANGELOG.md 为主线,梳理其从 1.0 到 7.0 的功能演进脉络,并结合 核心源码 与 测试用例 深入讲解每个配置项的实现原理、实战用法以及在 Egg 应用中的落地方式。读完本文,你将掌握如何独立使用该中间件为任意 Koa 应用提供高性能静态资源服务,并理解其缓存、压缩、条件请求的完整工作链路。
一、定位与血缘:从 koajs/static-cache 到 @eggjs/koa-static-cache
从 CHANGELOG.md 的早期条目可以看到,该包的历史可以追溯到 2013 年 12 月的1.0.0,彼时它还是 Koa 1 时代的koajs/static-cache。在 6.0.0 版本中,项目完成了两个关键动作:迁移为 TypeScript、包名变更为@eggjs/koa-static-cache,正如 README.md 所注明的:Forked from koajs/static-cache, refactor with TypeScript to support CommonJS and ESM both。
根据 README.md,它与koajs/static这类普通静态中间件的本质区别在于:
- 不支持目录浏览和
index.html自动索引; - 默认以流式方式输出,可选将文件内容常驻内存(
options.buffer); - 在初始化阶段就缓存全部资产,文件更新需要重启进程(可用
options.preload = false关闭); - 使用文件内容的 MD5 作为 ETag;
- 支持磁盘上的预压缩
.gz文件,类似 nginx 的gzip_static模块(对应 6.1.0 中引入的usePrecompiledGzip)。
该中间件在 plugins/static 中被描述为 "Static server plugin for egg, base on @eggjs/koa-static-cache",是整个 Egg 静态资源服务能力的地基。
二、版本演进时间线:CHANGELOG 背后的功能成长史
CHANGELOG.md 完整记录了十余年的演进,下面按阶段还原每一类核心能力是如何一步步沉淀出来的。
1.x:静态缓存的基础能力成形(2013–2014)
1.0.0(2013-12-21):基于yield* next的 Koa 1 风格中间件诞生;1.0.7(2014-03-26):新增options.gzip控制 gzip 压缩,支持 Buffer 与流两种形态的 gzip 输出;1.0.8(2014-03-31):新增options.dir,默认值为process.cwd();增加Vary响应头;按文件长度判断是否需要 gzip,并通过compressible判定类型可压缩性;1.0.9(2014-03-31):新增 url 前缀options.prefix;1.1.0(2014-07-16):以mime-types替换mime,移除 onerror/destroy 处理交由 Koa 负责;1.2.0(2014-09-18):以this.path作为 key 时先进行decodeURI解码。
2.x:协议与方法的收敛(2014–2017)
2.0.0(2014-11-14):升级 Koa,且仅响应 GET 与 HEAD 请求,其它方法直接放行给下游中间件;2.0.1(2014-12-02):容忍异常路径,例如//index.html这类双斜杠路径;2.0.2(2015-01-05):修复 Windows 平台下路径 normalize 的 bug。
3.x:动态加载与内存缓冲(2015)
3.0.0(2015-01-06):新增options.buffer = false以完全不缓存文件内容,仅流式输出;支持文件动态加载(对应dynamic雏形);3.0.1(2015-01-06):正式引入dynamic选项支持动态加载,并使用stat判断请求目标是否为文件夹;3.0.3(2015-03-28):修复动态模式下缓存未生效的问题;3.1.0(2015-03-28):合并 gzip 相关 PR(#33);3.1.1/3.0.2:连续修复 Windows 平台options.prefix的路径 bug;3.1.2(2015-07-08):修复动态文件场景下的报错;3.1.3(2015-11-26):修复 mtime 比较逻辑;3.1.5(2016-03-02):修复 Windows 平台动态加载文件的 bug;3.1.6(2016-03-22):不吞掉下游中间件的错误;3.2.0(2017-01-07):新增options.preload,控制初始化阶段是否预加载缓存;3.1.7(2016-04-07):升级mz至 2.4.0。
4.x–5.x:Koa 2 时代与工程化打磨(2017–2020)
4.0.0(2017-02-21):重构为先检查prefix再计算,避免无谓的路径运算;5.0.0(2017-04-01):正式支持 Koa 2;5.0.1(2017-04-19):支持 Node.js v7.6.0+;5.1.0(2017-06-01):files存储支持 LRU 淘汰;5.1.1(2017-06-13):只加载options.dir目录下的文件(安全边界);5.1.2(2018-02-06):依赖版本放宽为^;5.1.3(2020-04-29):修复preload = false时 alias 失效的问题;5.1.4(2020-08-03):清理无用 require、修正时间比较逻辑、修复 mtime(#93)。
6.x:TypeScript 化与现代工程改造(2025)
6.0.0(2025-01-12):drop Node.js < 18.19.0 support;通过tshy同时支持 CJS 与 ESM;迁移为@eggjs/koa-static-cache;升级 engines 至 18.19.0+;全面引入 TypeScript、ESLint、GitHub Actions 等工作流;6.1.0(2025-03-12):使用@eggjs/compressible进行可压缩性判定。
7.0.0+:面向 Egg 4 的收口
- 当前版本(见 package.json 为
7.0.2-beta.25)声明了明确的破坏性变更:- drop Node.js < 22.18.0 support;
- only support egg@4。
注意:CHANGELOG 顶部声明,后续版本的发布说明将改用 GitHub Releases 页面配合
release.yml工作流生成,CHANGELOG 文件本身不再逐条维护。
三、核心源码解析:一次静态缓存请求的完整链路
3.1 Options 全量配置说明
对照 src/index.ts 的Options接口,中间件支持的全部配置项如下:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
dir | string | process.cwd() | 静态资源根目录 |
maxAge | number | 0 | Cache-Control 的 max-age 秒数 |
cacheControl | string | function | undefined | 自定义 Cache-Control 头,优先级高于maxAge;传函数时以文件名为参数调用 |
buffer | boolean | false | 是否将文件内容缓存在内存中而非每次流式读取 |
gzip | boolean | false | 当请求Accept-Encoding含 gzip 时,运行时压缩响应 |
usePrecompiledGzip | boolean | false | 优先使用磁盘上的.gz预压缩文件(类似 nginxgzip_static) |
alias | object | {} | URL 别名映射 |
prefix | string | '' | URL 前缀 |
filter | function | string[] | undefined | 初始化扫描时的文件过滤器;数组形式表示白名单 |
dynamic | boolean | false | 是否支持请求时动态加载未缓存的新文件 |
preload | boolean | true | 是否在初始化时预加载全部文件,通常与dynamic配合使用 |
files | object | FileStore | undefined | 外部文件缓存对象,支持传入普通对象或带get/set的 LRU 存储 |
函数签名支持四种重载:staticCache()、staticCache(dir)、staticCache(options)、staticCache(dir, options, files),其中dir参数的优先级高于options.dir(这一点在 测试用例 中有专门验证)。
3.2 请求处理主链路
在 src/index.ts 中,中间件的主流程清晰可循:
- 方法过滤:仅接受
HEAD与GET,其它方法直接await next()放行; - 前缀检查:
ctx.path.startsWith(options.prefix)不满足则放行(这是 4.0.0 "check prefix first to avoid calculate" 的优化落地); - 路径归一化:先
decodeURIComponent解码中文等 URL 编码路径,再path.normalize处理//index这类异常路径; - 别名解析:命中
options.alias则替换 filename; - 缓存命中:命中
files.get(filename)直接使用;未命中且dynamic开启时,执行安全校验后loadFile动态加载; - 安全边界:动态加载前通过
fullpath.startsWith(dir)与stats.isFile()双重校验,确保只能访问options.dir之下的文件(这是 5.1.1 与 目录穿越测试 所保障的); - 条件请求:非 buffer 模式下每次请求会
fs.stat检查 mtime,若文件变更则清除旧的 md5/length;随后设置Last-Modified与 MD5ETag,若ctx.fresh为真则直接返回304; - 响应头:设置
Content-Type、Content-Length、Cache-Control(默认public, max-age=N)、Content-MD5; - gzip 决策:
enableGzip && file.length > 1024 && acceptGzip && compressible(file.type)满足时才压缩;压缩源优先取usePrecompiledGzip读到的磁盘.gz文件,否则用zlib.gzip实时压缩; - 输出:buffer 模式直接输出内存 Buffer,否则
createReadStream流式输出。
3.3 loadFile 与初始化预加载
loadFile 在初始化预加载(options.preload !== false)或动态加载时被调用,其职责包括:
- 以
path.join(options.prefix, name)作为缓存 key(因此 URL 与缓存 key 天然一致); - 用
mime-types推断 MIME 类型,兜底为application/octet-stream; - 记录
mtime、length,并计算文件内容的MD5(base64)作为 ETag 与 Content-MD5; options.cacheControl为函数时以文件名调用并缓存结果;options.buffer为真时把文件内容整体读入内存。
初始化扫描使用fs-readdir-recursive,默认跳过点开头的隐藏文件与node_modules目录(这正是测试中/.gitignore返回 404 的原因)。
四、实战:在 Koa 应用中独立使用
4.1 安装与最小示例
npm install @eggjs/koa-static-cacheconst path = require('path'); const { staticCache } = require('@eggjs/koa-static-cache'); app.use( staticCache(path.join(__dirname, 'public'), { maxAge: 365 * 24 * 60 * 60, }), );4.2 别名(Alias):免重定向的资源映射
当请求/favicon.png时需要返回/favicon-32.png,无需重定向、无需存储重复文件:
const options = { alias: { '/favicon.png': '/favicon-32.png', }, };别名处理在中间件主链路中位于路径归一化之后、缓存查询之前,因此别名 key 必须与归一化后的 URL 形态一致。
4.3 共享 files 对象:多目录合并与动态调整
合并多个目录进一个中间件(减少函数栈层级与哈希查找次数):
const files = {}; app.use(staticCache('/public/js', {}, files)); staticCache('/public/css', {}, files); // 追加更多文件到同一 files 对象运行时修改单个文件的缓存策略,例如单独调低/package.json的 maxAge:
const files = {}; app.use(staticCache('/public', { maxAge: 60 * 60 * 24 * 365 }, files)); files['/package.json'].maxAge = 60 * 60 * 24 * 30;这一能力在 测试用例 中得到验证:修改后请求/package.json会返回Cache-Control: public, max-age=1。
4.4 动态模式 + LRU:避免内存无限增长
dynamic: true时每次新文件请求都会写入缓存,若不设上限在大量不同文件场景下可能 OOM。README 推荐传入实现了get(key)/set(key, value)的 LRU 实例:
const LRU = require('lru-cache'); const files = new LRU({ max: 1000 }); app.use( staticCache({ dir: '/public', dynamic: true, files, }), );源码 中的FileManager会通过typeof store.set === 'function' && typeof store.get === 'function'自动识别传入的是 LRU 存储还是普通对象;动态 LRU 测试 验证了容量为 1 的 LRU 中旧条目会被正确淘汰。
4.5 filter 的两种形态
// 函数形态:自定义过滤逻辑(如跳过源码文件) staticCache({ dir: '/public', filter: (file) => !file.endsWith('.map') }); // 数组形态:仅白名单指定文件 staticCache({ dir: '/public', filter: ['index.html', 'app.js'] });数组形态在 src/index.ts 中被实现为options.filter.includes(file)的判定;filter 测试 验证了白名单之外的文件(如README.md)会返回 404。
五、在 Egg 框架中的落地:@eggjs/static 插件
@eggjs/koa-static-cache是 Egg 内置静态服务插件@eggjs/static的底层实现,见 plugins/static/package.json 中的"@eggjs/koa-static-cache": "workspace:*"依赖,以及 中间件源码 中直接调用staticCache(newOptions)。
5.1 Egg 侧的默认配置
根据 config.default.ts,Egg 中默认值如下:
prefix:'/public/'dir:path.join(appInfo.baseDir, 'app/public')dynamic:true(支持懒加载)preload:falsemaxAge: 生产环境31536000(一年),其它环境0(见 config.prod.ts)buffer: 生产环境true,其它环境falsemaxFiles:1000(仅dynamic开启时生效,作为 LRU 容量)
插件侧还通过koa-range中间件为静态资源补充 Range 请求支持,并用koa-compose将多个目录的 staticCache 组合为单一中间件。dir支持[dir1, dir2, ...]或[{ prefix: '/static2', dir: dir2 }]多目录数组形态,且目录不存在时会自动mkdirSync创建。
5.2 行为差异(重要)
- 非生产环境:资源不被缓存(
buffer: false、maxAge: 0、preload: false+dynamic: true),改动即时生效,便于开发调试; - 生产环境:
buffer: true且maxAge: 31536000,文件在首次访问后被缓存,更新静态资源需要重启进程。
这也是 CHANGELOG 中preload、dynamic、buffer等选项演进十多年后最终在 Egg 生态中的标准姿势。
六、关键行为速查:来自测试用例的证据
test/index.test.ts 中沉淀了一批值得在生产中注意的行为约定:
| 行为 | 测试依据 |
|---|---|
目录穿越被拦截(/%2E%2E/package.json返回 404) | L510-L520 |
隐藏文件(.gitignore)不提供服务 | L191-L193 |
带 query string 的请求正常处理(/src/index.ts?query=string) | L221-L223 |
携带If-None-Match的 HEAD/GET 返回 304 | L195-L211 |
| 非 GET/HEAD 方法放行给下游 | L217-L219 |
未开启dynamic时新增文件返回 404 | L324-L333 |
| 动态模式下新增文件返回 200 | L335-L355 |
动态模式下隐藏文件(.a.js)仍返回 404 | L402-L411 |
| 请求路径是文件夹时返回 404 | L427-L432 |
gzip 请求返回Content-Encoding: gzip且带Vary: Accept-Encoding,不支持的客户端收到原文 | L279-L311 |
| ETag 与 Content-MD5 等于文件内容 MD5 | L239-L244 |
七、升级注意事项与适用前提
结合 CHANGELOG 的破坏性变更声明,从旧版本升级到 7.x 需要关注:
- Node 版本:6.x 要求 Node.js ≥ 18.19.0,7.x 进一步提升为Node.js ≥ 22.18.0(package.json 的
engines字段同步声明); - Egg 版本:7.x 仅支持egg@4,升级前需确认框架版本;
- 包名变更:6.0.0 起包名从
koa-static-cache变为@eggjs/koa-static-cache,旧引入路径需同步更新; - 模块格式:6.0.0 起同时支持 CJS 与 ESM(
tshy双格式输出),main/module/types均已指向dist产物。
结语
从 2013 年的 Koa 1 中间件到如今支撑 Egg 4 的 TypeScript 现代实现,@eggjs/koa-static-cache的 CHANGELOG 本身就是一份完整的静态缓存工程实践档案。理解preload/dynamic的加载策略取舍、buffer的内存与实时性权衡、gzip 的三级压缩来源,以及 files/LRU 的缓存管理方式,将帮助你在独立 Koa 应用与 Egg 框架两个层面都能把静态资源服务调教到最优。
【免费下载链接】egg🥚🥚🥚🥚 Born to build better enterprise frameworks and apps with Node.js & Koa. https://307.run/eggcode项目地址: https://gitcode.com/gh_mirrors/eg/egg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考