需求背景与痛点分析
在Egg.js项目中,我们常使用官方自带的static 中间件来托管静态资源。该插件默认在生产环境为所有静态资源设置Cache-Control: public, max-age=31536000
(1年缓存),而开发环境则是max-age=0
(禁用强缓存)。这种"一刀切"的策略在大部分场景是可以接受的:
- 前端资源在每次打包后文件名会带上自己的hash值,因此内容变更会让文件名变动,让缓存失效。
- 而入口文件html则是走模板引擎去渲染,不会走static中间件,不受缓存策略影响。
那剩下的少数场景怎么办呢?
说说我的场景:
最近接到个跨团队协作的需求,使用到
模块联邦
来保持各业务的独立CICD。而模块联邦的入口文件
remoteEntry.js
,我是托管在egg-static上的,会受缓存策略的影响。每次更新部署后,都需要用户“强制刷新”清除缓存才会更新内容,这是不可接受的。
我看不少人在issue中提出路径级缓存策略定制的需求/疑惑,看起来这是Egg.js开发者普遍面临的痛点。而官方人员给出的解决方案是:
- egg-static 只建议在开发环境或对内的小系统使用,正常的业务最好是用 cdn 托管静态文件
- 实在要用,你可以把 images 放到另一个目录,不在 public 下,然后自己 router 对应的路径,手动实例化和挂载这个 static middeware 到 router middleware 上
就让我们来看看如何突破@eggjs/static缓存策略局限性?
方案1:中间件支持配置不同的缓存策略
先不考虑官方人员给出的建议,通过Readme可以看到@eggjs/static是基于@eggjs/koa-static-cache
实现,并且说明支持其所有配置。
其中的files 字段可以让我们配置具体的文件路径的maxAge等配置。
// @eggjs/koa-static-cache 给出的配置方法
const files = {};
app.use(staticCache('/public', {
maxAge: 60 * 60 * 24 * 365
}, files));
files['/package.json'].maxAge = 60 * 60 * 24 * 30;
但该方案不推荐使用,通过查看源码,egg-static
在koa-static-cache
的原配置上封装了一层。
当不传files 字段时,会默认使用LRU 缓存算法,而LRU 是用来解决“启用动态模式时可能出现OOM 内存溢出”。
static 生产环境配置默认开启动态模式和Buffer模式,开启后"访问过的静态文件,会被缓存在内存中,方便下次访问",所以需要LRU 算法来避免缓存爆炸的问题。
当然你也可以把动态模式给关闭,再使用该files配置。
方案1.1:中间件支持配置多个dir,应用不同的缓存策略
Readme提及dir 字段
支持数组的类型入参,虽然官方文档未明确说明此配置和缓存策略有关系(从字段名也看不出有关系)。
但过阅读@eggjs/static
源码,发现其核心配置项dir
是支持多目录动态挂载的特性,并且不同目录的缓存策略是可以独立配置的。
源码:遍历dirs 数组配置,每次遍历static 中间件会单独一次挂载,不同的dir 配置互不干扰。
这意味着可以通过分目录挂载+独立配置实现差异化缓存策略,下面分享一下具体的配置。
访问/mf
路由则会返回maxAge: 0的响应头,而/plublic 路径还是缓存1年。
// config.prod.js
const config = {};
const distPath = path_1.default.resolve(appInfo.baseDir, '..', 'dist');
const mfDistPath = path_1.default.resolve(appInfo.baseDir, '..', 'mf');
config.static = {
dir: [
distPath,
{
prefix: '/mf',
dir: mfDistPath,
maxAge: 0,
// dirObj类型传参:需要把 egg-static 的默认配置手动引进来
dynamic: true,
preload: false,
buffer: true,
maxFiles: 1000,
},
],
};
方案2:修改static 中间件返回的响应头
方案1.1
中我们实现了对特定目录应用不同的缓存策略,那能不能对特定文件也应用不同缓存策略呢?
我们都知道Egg.js 是基于Koa架构的,也就是中间件执行顺序会按照“洋葱圈模型”。理论上我们只在egg-static 后面加一个中间件,并针对请求路径改写响应头的“Cache-Control”字段就能满足我们的需求。让我们来实践一下。
// middleware/cache.ts
export default (options, app) => {
const cache = async (ctx: Context, next: () => Promise<void>) => {
await next();
if (ctx.path.endsWith("remoteEntry.js")) {
ctx.set('Cache-Control', 'no-cache');
}
};
return cache;
};
// app.ts
export default class App implements IBoot {
// https://eggjs.github.io/zh/guide/lifecycle.html
readonly app: Application;
constructor(app: Application) {
this.app = app;
}
async configDidLoad() {
const { config } = this.app;
const index = config.coreMiddleware.indexOf('static');
if (index > -1) {
// 插入cache 中间件到static 后面
config.coreMiddleware.splice(index, 0, 'cache');
}
}
}
很遗憾,但该方案其实是不行的!经过排查static 插件在执行完后所有逻辑后,没有再执行next()
。所以请求在static 中执行完就结束了,在他后面的中间件统统不会接收到该请求,请求头也无法再修改。
方案2.1:通过fork static 中间件来修改返回的响应头
官网人员提及到“手动实例化 static 中间件”给了我灵感。既然static 原插件不行,那我搞个forkStatic
来替换掉static 不就OK了。废话不多说,上代码~
// middleware/forkStatic.ts
import { Context } from '@nexus/egg';
import staticEgg from 'egg-static/app/middleware/static';
export default (_options, app) => {
const noCachePaths = ['/mf/mf-manifest.json', '/mf/remoteEntry.js'];
const staticMid = staticEgg(app.config['static'], app); // 复用static配置
const forkStatic = async (ctx: Context, next: () => Promise<void>) => {
await staticMid(ctx, next);
if (noCachePaths.some((path) => ctx.path.endsWith(path))) {
// ctx.set('Cache-Control', 'no-store'); // 关闭强缓存&协商缓存
ctx.set('Cache-Control', 'no-cache'); // 只关闭强缓存
}
};
return forkStatic;
};
// app.ts
export default class App implements IBoot {
// https://eggjs.github.io/zh/guide/lifecycle.html
readonly app: Application;
constructor(app: Application) {
this.app = app;
}
async configDidLoad() {
const { config } = this.app;
const index = config.coreMiddleware.indexOf('static');
if (index > -1) {
config.coreMiddleware[index] = 'forkStatic'; // 替代原来的static 中间件
}
}
}
结语
上面介绍了两种有效的方案来“突破@eggjs/static缓存策略局限性”,希望这个问题不再困扰你们。