Files

1025 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @fileoverview API 响应缓存中间件
*
* 来源: 基于 [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 修改
*
* 本模块提供 Express 中间件级别的 API 响应缓存功能,支持:
* - 内存缓存(默认)和 Redis 缓存(可选)
* - 按时间字符串(如 "2 minutes"、"1 hour")设置缓存过期
* - 按分组管理缓存条目,支持批量清除
* - 自定义缓存条件(状态码过滤、请求/响应 toggle 函数)
* - 缓存命中率统计(可选,用于性能监控)
* - ETag/304 协商缓存支持
* - JSONP 请求的 URL 去参处理
*
* 使用示例:
* const cache = require('./apicache').middleware;
* app.use(cache('2 minutes', (req, res) => res.statusCode === 200));
*
* @module apicache
* @requires url - URL 解析(用于 JSONP 模式下去除查询参数)
* @requires ./memory-cache - 内存缓存实现
*/
const url = require('url');
const MemoryCache = require('./memory-cache');
/**
* 时间单位到毫秒的转换表
* 用于解析人类可读的时间字符串(如 "2 minutes"、"1 hour"
* @type {Object.<string, number>}
*/
const t = {
ms: 1, // 毫秒
second: 1000, // 秒 → 1000ms
minute: 60000, // 分钟 → 60000ms
hour: 3600000, // 小时 → 3600000ms
day: 3600000 * 24, // 天 → 86400000ms
week: 3600000 * 24 * 7, // 周 → 604800000ms
month: 3600000 * 24 * 30, // 月(30天)→ 2592000000ms
};
/**
* 所有 ApiCache 实例的全局注册表
* 用于跟踪和管理多个缓存实例
* @type {Array<ApiCache>}
*/
const instances = [];
/**
* 创建严格相等匹配函数
* @param {string} a - 要匹配的值
* @returns {function(string): boolean} 匹配函数
*/
const matches = function (a) {
return function (b) {
return a === b;
};
};
/**
* 创建不匹配函数(matches 的反函数)
* @param {string} a - 要排除的值
* @returns {function(string): boolean} 不匹配函数
*/
const doesntMatch = function (a) {
return function (b) {
return !matches(a)(b);
};
};
/**
* 格式化持续时间为人类可读的日志字符串
* 超过 1000ms 显示为秒(如 "1.23sec"),否则显示为毫秒(如 "456ms"
* 输出带黄色 ANSI 转义码
*
* @param {number} d - 持续时间(毫秒)
* @param {string} [prefix] - 可选的前缀文本
* @returns {string} 带颜色的日志字符串
*/
const logDuration = function (d, prefix) {
const str = d > 1000 ? `${(d / 1000).toFixed(2)}sec` : `${d}ms`;
return `\x1B[33m- ${prefix ? `${prefix} ` : ''}${str}\x1B[0m`;
};
/**
* 安全地获取响应头对象
* 兼容不同版本的 Express/Node.js
* @param {Object} res - Express 响应对象
* @returns {Object} 响应头键值对
*/
function getSafeHeaders(res) {
return res.getHeaders ? res.getHeaders() : res._headers;
}
/**
* ApiCache 缓存中间件构造函数
*
* 每个实例维护独立的:
* - 内存缓存(MemoryCache
* - 全局配置选项
* - 缓存索引(all + groups
* - 定时器集合(用于自动过期清除)
* - 性能统计数组
*
* @constructor
*/
function ApiCache() {
const memCache = new MemoryCache();
/**
* 全局配置选项
* @type {Object}
*/
const globalOptions = {
debug: false, // 是否开启调试日志
defaultDuration: 3600000, // 默认缓存时长(1小时,毫秒)
enabled: true, // 是否启用缓存
appendKey: [], // 自定义缓存键追加字段(数组或函数)
jsonp: false, // 是否为 JSONP 请求(启用后会去除 URL 中的查询参数)
redisClient: false, // Redis 客户端实例(false 表示使用内存缓存)
headerBlacklist: [], // 不缓存的响应头黑名单
statusCodes: {
include: [], // 仅缓存这些状态码(空数组表示不限制)
exclude: [], // 排除这些状态码
},
events: {
expire: undefined, // 缓存过期时的回调函数
},
headers: {}, // 强制覆盖的响应头(如 'cache-control': 'no-cache'
trackPerformance: false, // 是否跟踪缓存命中率(会增加内存消耗)
};
const middlewareOptions = []; // 所有中间件的选项注册表
const instance = this;
let index = null; // 缓存索引:{ all: [key...], groups: { groupName: [key...] } }
const timers = {}; // 自动过期定时器集合 { key: setTimeout ID }
const performanceArray = []; // 缓存命中率统计数组
// 将当前实例注册到全局实例列表
instances.push(this);
this.id = instances.length;
/**
* 调试日志输出函数
* 仅在 debug 模式或环境变量 DEBUG 包含 'apicache' 时输出
* @param {...*} args - 日志参数
*/
function debug(a, b, c, d) {
const arr = ['\x1B[36m[apicache]\x1B[0m', a, b, c, d].filter((arg) => {
return arg !== undefined;
});
const debugEnv = process.env.DEBUG && process.env.DEBUG.split(',').includes('apicache');
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
}
/**
* 判断响应是否应该被缓存
*
* 判断逻辑:
* 1. 响应对象必须存在
* 2. 如果提供了 toggle 函数,toggle 必须返回 true
* 3. 响应状态码不在 exclude 列表中
* 4. 响应状态码在 include 列表中(如果 include 非空)
*
* @param {Object} request - Express 请求对象
* @param {Object} response - Express 响应对象
* @param {function} [toggle] - 可选的缓存条件函数 (req, res) => boolean
* @returns {boolean} 是否应该缓存
*/
function shouldCacheResponse(request, response, toggle) {
const opt = globalOptions;
const codes = opt.statusCodes;
if (!response) {
return false;
}
// 自定义 toggle 函数判断
if (toggle && !toggle(request, response)) {
return false;
}
// 状态码排除列表
if (codes.exclude && codes.exclude.length && codes.exclude.includes(response.statusCode)) {
return false;
}
// 状态码包含列表(白名单模式)
if (codes.include && codes.include.length && codes.include.includes(response.statusCode)) {
return false;
}
return true;
}
/**
* 将缓存键添加到索引中
*
* 索引结构:
* - index.all: 所有缓存键的数组(用于全量清除)
* - index.groups: 按分组名索引的缓存键数组(用于按组清除)
*
* @param {string} key - 缓存键
* @param {Object} req - Express 请求对象(req.apicacheGroup 可指定分组名)
*/
function addIndexEntries(key, req) {
const groupName = req.apicacheGroup;
if (groupName) {
debug(`group detected "${groupName}"`);
// 将键添加到分组索引(unshift 添加到数组头部)
const group = (index.groups[groupName] = index.groups[groupName] || []);
group.unshift(key);
}
// 将键添加到全局索引
index.all.unshift(key);
}
/**
* 过滤掉黑名单中的响应头
* @param {Object} headers - 原始响应头
* @returns {Object} 过滤后的响应头
*/
function filterBlacklistedHeaders(headers) {
return Object.keys(headers)
.filter((key) => {
return !globalOptions.headerBlacklist.includes(key);
})
.reduce((acc, header) => {
acc[header] = headers[header];
return acc;
}, {});
}
/**
* 创建缓存对象
*
* @param {number} status - HTTP 状态码
* @param {Object} headers - 响应头
* @param {string|Buffer} data - 响应体数据
* @param {string} encoding - 编码方式
* @returns {Object} 缓存对象
*/
function createCacheObject(status, headers, data, encoding) {
return {
status,
headers: filterBlacklistedHeaders(headers), // 过滤黑名单头
data,
encoding,
timestamp: new Date().getTime() / 1000, // Unix 时间戳(秒),用于计算剩余 max-age
};
}
/**
* 将响应数据存入缓存
*
* 支持两种存储后端:
* - Redis: 使用 hset 存储,expire 设置过期
* - 内存: 使用 MemoryCache 存储
*
* 同时设置自动清除定时器(setTimeout 最大值限制为 2147483647ms ≈ 24.8 天)
*
* @param {string} key - 缓存键
* @param {Object} value - 缓存对象
* @param {number} duration - 缓存时长(毫秒)
*/
function cacheResponse(key, value, duration) {
const redis = globalOptions.redisClient;
const expireCallback = globalOptions.events.expire;
if (redis && redis.connected) {
// Redis 存储
try {
redis.hset(key, 'response', JSON.stringify(value));
redis.hset(key, 'duration', duration);
redis.expire(key, duration / 1000, expireCallback || (() => {}));
} catch (err) {
debug('[apicache] error in redis.hset()');
}
} else {
// 内存存储
memCache.add(key, value, duration, expireCallback);
}
// 设置自动清除定时器(限制最大值防止 setTimeout 溢出)
timers[key] = setTimeout(() => {
instance.clear(key, true);
}, Math.min(duration, 2147483647));
}
/**
* 累积响应内容到 res._apicache.content
*
* Express 的 res.write() 可能被多次调用,每次传递部分内容。
* 此函数将所有部分内容累积起来,用于最终缓存完整的响应。
*
* 支持三种内容类型:
* - string: 字符串拼接
* - Buffer: Buffer.concat 拼接
* - 其他: 直接赋值(如 JSON 对象)
*
* @param {Object} res - Express 响应对象
* @param {string|Buffer} content - 本次写入的内容
*/
function accumulateContent(res, content) {
if (content) {
if (typeof content == 'string') {
// 字符串拼接
res._apicache.content = (res._apicache.content || '') + content;
} else if (Buffer.isBuffer(content)) {
let oldContent = res._apicache.content;
// 将旧的字符串内容转为 Buffer
if (typeof oldContent === 'string') {
oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent);
}
if (!oldContent) {
oldContent = !Buffer.alloc ? Buffer.alloc(0) : Buffer.alloc(0);
}
// Buffer 拼接
res._apicache.content = Buffer.concat([oldContent, content], oldContent.length + content.length);
} else {
// 其他类型直接赋值
res._apicache.content = content;
}
}
}
/**
* 使响应变得可缓存(Monkey-patch res 的方法)
*
* 通过劫持 res.writeHead、res.write、res.end 三个方法,
* 在响应完成时自动将内容存入缓存。
*
* 流程:
* 1. 保存原始方法引用到 res._apicache
* 2. 应用全局响应头覆盖
* 3. 重写 res.writeHead: 添加 cache-control 头,保存响应头快照
* 4. 重写 res.write: 累积部分内容
* 5. 重写 res.end: 判断是否缓存,创建缓存对象并存储
*
* @param {Object} req - Express 请求对象
* @param {Object} res - Express 响应对象
* @param {function} next - Express next 函数
* @param {string} key - 缓存键
* @param {number} duration - 缓存时长(毫秒)
* @param {string} strDuration - 原始时长字符串(用于日志)
* @param {function} [toggle] - 缓存条件函数
*/
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) {
// 保存原始方法引用,用于后续调用
res._apicache = {
write: res.write,
writeHead: res.writeHead,
end: res.end,
cacheable: true,
content: undefined,
};
// 应用全局响应头覆盖
Object.keys(globalOptions.headers).forEach((name) => {
res.setHeader(name, globalOptions.headers[name]);
});
// 重写 res.writeHead:添加 cache-control 头并保存响应头快照
res.writeHead = function () {
if (!globalOptions.headers['cache-control']) {
if (shouldCacheResponse(req, res, toggle)) {
// 可缓存:设置 max-age
res.setHeader('cache-control', `max-age=${(duration / 1000).toFixed(0)}`);
} else {
// 不可缓存:禁止缓存
res.setHeader('cache-control', 'no-cache, no-store, must-revalidate');
}
}
// 保存响应头快照(在 writeHead 被调用后,headers 已确定)
res._apicache.headers = Object.assign({}, getSafeHeaders(res));
return res._apicache.writeHead.apply(this, arguments);
};
// 重写 res.write:累积部分内容
res.write = function (content) {
accumulateContent(res, content);
return res._apicache.write.apply(this, arguments);
};
// 重写 res.end:判断是否缓存,创建缓存对象并存储
res.end = function (content, encoding) {
if (shouldCacheResponse(req, res, toggle)) {
accumulateContent(res, content);
if (res._apicache.cacheable && res._apicache.content) {
// 添加到缓存索引
addIndexEntries(key, req);
const headers = res._apicache.headers || getSafeHeaders(res);
// 创建缓存对象并存储
const cacheObject = createCacheObject(res.statusCode, headers, res._apicache.content, encoding);
cacheResponse(key, cacheObject, duration);
// 调试日志
const elapsed = new Date() - req.apicacheTimer;
debug(`adding cache entry for "${key}" @ ${strDuration}`, logDuration(elapsed));
debug('_apicache.headers: ', res._apicache.headers);
debug('res.getHeaders(): ', getSafeHeaders(res));
debug('cacheObject: ', cacheObject);
}
}
// 调用原始 res.end
return res._apicache.end.apply(this, arguments);
};
next();
}
/**
* 发送缓存的响应
*
* 处理逻辑:
* 1. 检查 toggle 函数是否允许返回缓存
* 2. 合并响应头,正确递减 max-age(减去已过去的时间)
* 3. 反序列化 Buffer 数据
* 4. 检查 ETag/If-None-Match,满足条件返回 304
* 5. 返回完整的缓存响应
*
* @param {Object} request - Express 请求对象
* @param {Object} response - Express 响应对象
* @param {Object} cacheObject - 缓存对象
* @param {function} [toggle] - 缓存条件函数
* @param {function} next - Express next 函数
* @param {number} duration - 缓存时长(毫秒)
*/
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) {
if (toggle && !toggle(request, response)) {
return next();
}
const headers = getSafeHeaders(response);
// 合并缓存的响应头,并正确递减 max-age
Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {}), {
// max-age = 原始时长 - 已过去的时间,最小为 0
'cache-control': `max-age=${Math.max(0, (duration / 1000 - (new Date().getTime() / 1000 - cacheObject.timestamp)).toFixed(0))}`,
});
// 反序列化 Buffer 数据(JSON 序列化后的 Buffer 会变成 { type: 'Buffer', data: [...] } 格式)
let data = cacheObject.data;
if (data && data.type === 'Buffer') {
data = typeof data.data === 'number' ? new Buffer.alloc(data.data) : new Buffer.from(data.data);
}
// ETag 协商缓存:如果请求的 If-None-Match 与缓存的 ETag 匹配,返回 304
const cachedEtag = cacheObject.headers.etag;
const requestEtag = request.headers['if-none-match'];
if (requestEtag && cachedEtag === requestEtag) {
response.writeHead(304, headers);
return response.end();
}
// 返回缓存的响应
response.writeHead(cacheObject.status || 200, headers);
return response.end(data, cacheObject.encoding);
}
/**
* 同步全局选项到所有中间件实例
* 将 globalOptions 合并到每个中间件的 opt 中,并应用各自的 localOptions
*/
function syncOptions() {
for (const i in middlewareOptions) {
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions);
}
}
/**
* 清除缓存
*
* 支持三种清除模式:
* 1. 按分组名清除: target 为分组名,清除该分组下的所有缓存
* 2. 按缓存键清除: target 为具体的缓存键
* 3. 全部清除: target 为空,清除所有缓存
*
* @param {string} [target] - 分组名或缓存键,为空则清除全部
* @param {boolean} [isAutomatic] - 是否为自动过期清除(用于日志区分)
* @returns {Object} 当前缓存索引
*/
this.clear = function (target, isAutomatic) {
const group = index.groups[target];
const redis = globalOptions.redisClient;
if (group) {
// 模式1: 按分组名清除
debug(`clearing group "${target}"`);
group.forEach((key) => {
debug(`clearing cached entry for "${key}"`);
clearTimeout(timers[key]);
delete timers[key];
if (!globalOptions.redisClient) {
memCache.delete(key);
} else {
try {
redis.del(key);
} catch (err) {
console.log(`[apicache] error in redis.del("${key}")`);
}
}
index.all = index.all.filter(doesntMatch(key));
});
delete index.groups[target];
} else if (target) {
// 模式2: 按缓存键清除
debug(`clearing ${isAutomatic ? 'expired' : 'cached'} entry for "${target}"`);
clearTimeout(timers[target]);
delete timers[target];
if (!redis) {
memCache.delete(target);
} else {
try {
redis.del(target);
} catch (err) {
console.log(`[apicache] error in redis.del("${target}")`);
}
}
// 从全局索引中移除
index.all = index.all.filter(doesntMatch(target));
// 从所有分组中移除,并清理空分组
Object.keys(index.groups).forEach((groupName) => {
index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target));
if (!index.groups[groupName].length) {
delete index.groups[groupName];
}
});
} else {
// 模式3: 全部清除
debug('clearing entire index');
Object.keys(timers).forEach((key) => {
clearTimeout(timers[key]);
delete timers[key];
});
if (!redis) {
memCache.clear();
} else {
// 逐个清除 Redis 键(避免误删非 apicache 的条目)
index.all.forEach((key) => {
try {
redis.del(key);
} catch (err) {
console.log(`[apicache] error in redis.del("${key}")`);
}
});
}
this.resetIndex();
}
return this.getIndex();
};
/**
* 解析时长字符串为毫秒数
*
* 支持的格式:
* - 数字: 直接返回(毫秒)
* - 字符串: "2 minutes"、"1 hour"、"30 seconds"、"500ms"
* - 数字部分支持小数点(如 "1.5 hours"
* - 单位支持复数(自动去掉末尾的 s)
* - "m" 会被解析为 "ms"(毫秒缩写)
*
* @param {number|string} duration - 时长(数字或字符串)
* @param {number} defaultDuration - 默认时长(解析失败时使用)
* @returns {number} 时长(毫秒)
*/
function parseDuration(duration, defaultDuration) {
if (typeof duration === 'number') {
return duration;
}
if (typeof duration === 'string') {
const split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
if (split.length === 3) {
const len = Number.parseFloat(split[1]);
let unit = split[2].replace(/s$/i, '').toLowerCase();
if (unit === 'm') {
unit = 'ms';
}
return (len || 1) * (t[unit] || 0);
}
}
return defaultDuration;
}
/**
* 获取时长(公开方法)
* @param {number|string} duration - 时长
* @returns {number} 解析后的毫秒数
*/
this.getDuration = function (duration) {
return parseDuration(duration, globalOptions.defaultDuration);
};
/**
* 获取缓存性能统计(命中率)
*
* 可用于创建性能监控接口:
* app.get('/api/cache/performance', (req, res) => {
* res.json(apicache.getPerformance())
* })
*
* @returns {Array<Object>} 每个中间件的性能报告数组
*/
this.getPerformance = function () {
return performanceArray.map((p) => {
return p.report();
});
};
/**
* 获取缓存索引
* @param {string} [group] - 可选的分组名,不传则返回完整索引
* @returns {Object} 缓存索引
*/
this.getIndex = function (group) {
if (group) {
return index.groups[group];
} else {
return index;
}
};
/**
* 创建缓存中间件
*
* 这是主要的公开 API,返回一个 Express 中间件函数。
* 中间件会拦截请求,检查缓存,命中则直接返回缓存响应,未命中则继续处理并缓存响应。
*
* @param {string|number} strDuration - 缓存时长(如 "2 minutes" 或 120000
* @param {function} [middlewareToggle] - 中间件级别的缓存条件函数 (req, res) => boolean
* @param {Object} [localOptions] - 本中间件的局部配置(覆盖全局配置)
* @returns {function} Express 中间件函数
*/
this.middleware = function cache(strDuration, middlewareToggle, localOptions) {
const duration = instance.getDuration(strDuration);
const opt = {};
// 注册中间件选项
middlewareOptions.push({
options: opt,
});
/**
* 更新或获取中间件选项
* @param {Object} [localOptions] - 局部配置
* @returns {Object} 当前中间件的配置
*/
const options = function (localOptions) {
if (localOptions) {
middlewareOptions.find((middleware) => {
return middleware.options === opt;
}).localOptions = localOptions;
}
syncOptions();
return opt;
};
options(localOptions);
/**
* 空操作的性能统计类(不跟踪性能时使用)
* 所有方法都是空函数,避免条件判断开销
*/
function NOOPCachePerformance() {
this.report = this.hit = this.miss = function () {}; // noop;
}
/**
* 缓存命中率统计类
*
* 使用位压缩技术高效存储历史命中/未命中记录:
* - 每个 Uint8Array 元素存储 4 条记录
* - 每条记录占 2 bit: 00=无记录, 01=命中, 10=未命中
* - 支持 4 个时间窗口: 最近 100/1000/10000/100000 次请求
*/
function CachePerformance() {
/**
* 最近 100 次请求的命中记录(位压缩存储)
* @type {Uint8Array}
*/
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
/**
* 最近 1000 次请求的命中记录
* @type {Uint8Array}
*/
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
/**
* 最近 10000 次请求的命中记录
* @type {Uint8Array}
*/
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
/**
* 最近 100000 次请求的命中记录
* @type {Uint8Array}
*/
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
/**
* 总请求次数(自服务器启动以来)
* @type {number}
*/
this.callCount = 0;
/**
* 总命中次数
* @type {number}
*/
this.hitCount = 0;
/**
* 最后一次缓存命中的键(用于识别对应路由)
* @type {string|null}
*/
this.lastCacheHit = null;
/**
* 最后一次缓存未命中的键
* @type {string|null}
*/
this.lastCacheMiss = null;
/**
* 生成性能统计报告
* @returns {Object} 包含各项统计指标的报告对象
*/
this.report = function () {
return {
lastCacheHit: this.lastCacheHit,
lastCacheMiss: this.lastCacheMiss,
callCount: this.callCount,
hitCount: this.hitCount,
missCount: this.callCount - this.hitCount,
hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
hitRateLast100: this.hitRate(this.hitsLast100),
hitRateLast1000: this.hitRate(this.hitsLast1000),
hitRateLast10000: this.hitRate(this.hitsLast10000),
hitRateLast100000: this.hitRate(this.hitsLast100000),
};
};
/**
* 从位压缩数组中计算命中率
*
* 遍历每个字节的 4 个 2-bit 记录:
* - 01 (1) = 命中
* - 10 (2) = 未命中
* - 00 (0) = 无记录(忽略)
*
* @param {Uint8Array} array - 位压缩的命中记录数组
* @returns {number|null} 命中率(0~1),无记录时返回 null
*/
this.hitRate = function (array) {
let hits = 0;
let misses = 0;
for (let i = 0; i < array.length; i++) {
let n8 = array[i];
for (j = 0; j < 4; j++) {
switch (n8 & 3) {
case 1:
hits++;
break;
case 2:
misses++;
break;
}
n8 >>= 2;
}
}
const total = hits + misses;
if (total == 0) {
return null;
}
return hits / total;
};
/**
* 在位压缩数组中记录一次命中或未命中
*
* 编码规则(每个 2-bit 记录):
* - 00: 无记录
* - 01: 命中
* - 10: 未命中
*
* @param {Uint8Array} array - 位压缩数组
* @param {boolean} hit - true=命中, false=未命中
*/
this.recordHitInArray = function (array, hit) {
const arrayIndex = ~~(this.callCount / 4) % array.length; // 数组索引
const bitOffset = (this.callCount % 4) * 2; // 位偏移(每 2 bit 一条记录)
const clearMask = ~(3 << bitOffset); // 清除掩码
const record = (hit ? 1 : 2) << bitOffset; // 编码记录
array[arrayIndex] = (array[arrayIndex] & clearMask) | record;
};
/**
* 记录一次命中/未命中到所有时间窗口的数组
* @param {boolean} hit - true=命中, false=未命中
*/
this.recordHit = function (hit) {
this.recordHitInArray(this.hitsLast100, hit);
this.recordHitInArray(this.hitsLast1000, hit);
this.recordHitInArray(this.hitsLast10000, hit);
this.recordHitInArray(this.hitsLast100000, hit);
if (hit) {
this.hitCount++;
}
this.callCount++;
};
/**
* 记录一次缓存命中
* @param {string} key - 命中的缓存键
*/
this.hit = function (key) {
this.recordHit(true);
this.lastCacheHit = key;
};
/**
* 记录一次缓存未命中
* @param {string} key - 未命中的缓存键
*/
this.miss = function (key) {
this.recordHit(false);
this.lastCacheMiss = key;
};
}
// 根据配置决定使用真实的性能统计还是空操作
const perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance();
performanceArray.push(perf);
/**
* 核心缓存中间件函数
*
* 请求处理流程:
* 1. 检查缓存是否启用、是否有 bypass 头
* 2. 生成缓存键(hostname + URL + appendKey
* 3. 尝试从内存缓存获取 → 命中则直接返回
* 4. 尝试从 Redis 获取 → 命中则直接返回
* 5. 未命中 → 劫持 res 方法,等待响应完成后缓存
*/
const cache = function (req, res, next) {
/**
* 跳过缓存,直接进入下一个中间件
*/
function bypass() {
debug('bypass detected, skipping cache.');
return next();
}
// 初始跳过检查
if (!opt.enabled) {
return bypass();
}
// 通过请求头强制跳过缓存
if (req.headers['x-apicache-bypass'] || req.headers['x-apicache-force-fetch']) {
return bypass();
}
// 记录请求开始时间(用于计算耗时)
req.apicacheTimer = new Date();
// 生成缓存键: hostname + URL
// Express 4.x 中 req.url 可能是路由相对路径,使用 originalUrl 获取完整路径
let key = req.hostname + (req.originalUrl || req.url);
// JSONP 模式:去除查询参数(避免不同的 callback 参数产生不同的缓存键)
if (opt.jsonp) {
key = url.parse(key).pathname;
}
// 追加自定义缓存键(支持函数或属性路径数组)
if (typeof opt.appendKey === 'function') {
key += `$$appendKey=${opt.appendKey(req, res)}`;
} else if (opt.appendKey.length > 0) {
let appendKey = req;
for (let i = 0; i < opt.appendKey.length; i++) {
appendKey = appendKey[opt.appendKey[i]];
}
key += `$$appendKey=${appendKey}`;
}
// 尝试从缓存获取
const redis = opt.redisClient;
const cached = !redis ? memCache.getValue(key) : null;
// 内存缓存命中
if (cached) {
const elapsed = new Date() - req.apicacheTimer;
debug('sending cached (memory-cache) version of', key, logDuration(elapsed));
perf.hit(key);
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration);
}
// Redis 缓存命中
if (redis && redis.connected) {
try {
redis.hgetall(key, (err, obj) => {
if (!err && obj && obj.response) {
const elapsed = new Date() - req.apicacheTimer;
debug('sending cached (redis) version of', key, logDuration(elapsed));
perf.hit(key);
return sendCachedResponse(req, res, JSON.parse(obj.response), middlewareToggle, next, duration);
} else {
// Redis 未命中,进入缓存写入流程
perf.miss(key);
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
}
});
} catch (err) {
// Redis 出错时降级为未命中
perf.miss(key);
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
}
} else {
// 无 Redis,内存也未命中,进入缓存写入流程
perf.miss(key);
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle);
}
};
// 暴露 options 函数,允许运行时修改配置
cache.options = options;
return cache;
};
/**
* 设置或获取全局配置选项
*
* @param {Object} [options] - 配置对象,不传则返回当前配置
* @returns {ApiCache|Object} 传入 options 时返回 this(支持链式调用),否则返回当前配置
*/
this.options = function (options) {
if (options) {
Object.assign(globalOptions, options);
syncOptions();
if ('defaultDuration' in options) {
// 将默认时长转换为毫秒数
globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000);
}
if (globalOptions.trackPerformance) {
debug('WARNING: using trackPerformance flag can cause high memory usage!');
}
return this;
} else {
return globalOptions;
}
};
/**
* 重置缓存索引(清空所有索引记录)
*/
this.resetIndex = function () {
index = {
all: [], // 所有缓存键
groups: {}, // 分组索引 { groupName: [key...] }
};
};
/**
* 创建新的 ApiCache 实例(可选配置)
* @param {Object} [config] - 初始配置
* @returns {ApiCache} 新实例
*/
this.newInstance = function (config) {
const instance = new ApiCache();
if (config) {
instance.options(config);
}
return instance;
};
/**
* 克隆当前实例(复制配置)
* @returns {ApiCache} 克隆的实例
*/
this.clone = function () {
return this.newInstance(this.options());
};
// 初始化缓存索引
this.resetIndex();
}
// 导出单例实例(整个应用共享一个缓存管理器)
module.exports = new ApiCache();