帮你快速理解、总结文档立即下载

缓存 GraphQL 请求

最近更新时间:2026-11-02 10:33:09
我的收藏
该示例对 GraphQL 查询的请求体(query、variables、operationName)计算 SHA-256 摘要,作为缓存键的一部分,并使用 Cache API 将响应内容写入边缘函数独立缓存:命中缓存时直接返回缓存内容,未命中时使用 Fetch API 发起子请求获取数据并写入缓存;mutation、subscription 等会修改数据的请求不缓存。可通过响应头 x-edgefunctions-cache 判断是否命中缓存。
说明:
读、写、删需使用同一个指定了 eo.cacheKey 的 Request 对象;未携带 eo.cacheKey 时,写入会被拒绝并抛出 forbidden cdn cache 错误,详见 Cache API。
GraphQL 请求通常共用一个请求端点(URL 相同),仅以 URL 作为缓存键会导致不同查询相互覆盖,因此缓存键需包含查询语句与变量;且缓存键 request 只支持 GET 方法,无法直接使用 POST 的 event.request,本示例将 URL、operationName、variables、query 摘要后一并写入 eo.cacheKey。
仅缓存查询(query):mutation、subscription 会修改数据、不能缓存(示例按操作类型做简化判断,生产环境建议使用 GraphQL 解析库或查询白名单);携带 Authorization、Cookie 等身份信息的请求默认跳过缓存,避免不同用户命中同一份缓存。

示例代码

// 缓存时长(秒),可按业务调整
const CACHE_TTL = 10;

// 携带用户身份信息的请求默认跳过缓存,避免不同用户命中同一份缓存
const SKIP_PRIVATE_REQUESTS = true;

function uint8ArrayToHex(arr) {
return Array.prototype.map.call(arr, (x) => (('0' + x.toString(16)).slice(-2))).join('');
}

// sha256 签名摘要
async function sha256(message) {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);

return uint8ArrayToHex(new Uint8Array(hashBuffer));
}

// 解析 GraphQL 请求:非 POST、携带身份信息、非查询(mutation/subscription)等场景返回 null
async function parseGraphQLRequest(request) {
if (request.method.toUpperCase() !== 'POST') {
return null;
}

if (SKIP_PRIVATE_REQUESTS && (request.headers.has('Authorization') || request.headers.has('Cookie'))) {
return null;
}

let payload;
try {
// 读取 body 用于构造缓存键,后续 fetch 使用的是未被消费的原始请求
payload = await request.clone().json();
} catch (e) {
return null;
}

if (!payload || typeof payload.query !== 'string') {
return null;
}

// 去掉注释与空白后判断操作类型,mutation、subscription 不缓存
// 此处为简化判断,字段名或字符串中包含关键字等场景建议使用 GraphQL 解析库
const operation = payload.query.replace(/#[^\\n]*/g, ' ').trim();
if (/^(mutation|subscription)\\b/i.test(operation)) {
return null;
}

return payload;
}

async function fetchGraphQL(event, request, cacheKey) {
const cache = caches.default;

// 缓存没有命中,回源获取数据
const response = await fetch(request);

if (response.ok) {
// 设置缓存时长,确保回源响应可以写入缓存
response.headers.set('Cache-Control', `s-maxage=${CACHE_TTL}`);
event.waitUntil(cache.put(cacheKey, response.clone()));
}

// 未命中缓存,设置响应头标识
response.headers.append('x-edgefunctions-cache', 'miss');

return response;
}

async function handleRequest(event) {
const request = event.request;
const cache = caches.default;
const payload = await parseGraphQLRequest(request);

// 非查询请求直接回源,不做缓存
if (!payload) {
return fetch(request);
}

// 缓存键需唯一标识一次查询:URL + operationName + variables + query
const normalized = `${request.url}#${payload.operationName || ''}#${JSON.stringify(payload.variables || {})}#${payload.query}`;
const hash = await sha256(normalized);

// 使用查询摘要构造缓存键,并通过 eo.cacheKey 指定读写边缘函数独立缓存;
// 读、写、删需使用同一个 Request 对象
const cacheKey = new Request(request.url, {
eo: { cacheKey: hash },
});

try {
// 获取关联的缓存 Response
const response = await cache.match(cacheKey);

if (!response) {
return fetchGraphQL(event, request, cacheKey);
}

// 命中缓存,设置响应头标识
response.headers.append('x-edgefunctions-cache', 'hit');

return response;
} catch (error) {
// 缓存过期(504)等异常:删除旧缓存后重新回源并写入
try {
await cache.delete(cacheKey);
} catch (e) {}
return fetchGraphQL(event, request, cacheKey);
}
}

addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event));
});

示例预览

未命中缓存



命中缓存




相关参考