- 后端
- API设计
【免费下载链接】graphql-yoga
🧘 Rewrite of a fully-featured GraphQL Server with focus on easy setup, performance & great developer experience. The core of Yoga implements WHATWG Fetch API and can run/deploy on any JS environment.
导读
@envelop/validation-cache是 GraphQL Yoga 所基于的 Envelop 体系中的一个官方插件,它为 GraphQL 请求链路中开销较大的validate步骤引入 LRU 缓存,让相同操作的验证结果可以直接命中缓存而无需重复计算。本文将围绕该插件当前的 README 与 CHANGELOG,结合 核心实现、测试用例 与 GraphQL Yoga 的内置集成源码,完整讲解它的安装配置、缓存键构造原理、可定制接口以及版本演进中踩过的坑,读完即可在自己的服务中正确使用或替换该缓存。
插件定位:为什么要缓存验证结果
一个 GraphQL 请求在服务端通常要经历四个阶段:解析(parse)→ 验证(validate)→ 执行(execute)→ 订阅(subscribe)。其中验证阶段会针对整个 schema 运行一组ValidationRule,检查操作文档是否合法(字段是否存在、类型是否匹配、参数是否正确等)。当 schema 较大、规则较多时,验证成本不容忽视。
@envelop/validation-cache的定位正是缓存这一步骤:对相同的操作文档,跳过重复验证,直接复用上次的验证结果(包括错误结果)。根据插件 README 中基于基准测试的说明,使用该插件可提升验证性能约 50%;GraphQL Yoga 官方文档 Parsing and Validation Caching 亦指出,解析缓存可带来约 60% 的性能提升,验证缓存约 50%。
需要说明的是,该缓存保存的是验证结果(一组GraphQLError),而不是执行结果,因此不会影响数据新鲜度,属于安全且收益明显的优化手段。
安装与快速接入
安装
在项目的 package.json 中声明该依赖:
yarn add @envelop/validation-cache从 package.json 可以看到,它要求node >= 18.0.0,以@envelop/core为 peer 依赖,同时支持graphql的^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0全版本范围(自 10.2.0 起兼容 graphql-js 17)。
基础用法
在 Envelop 的插件链中注册即可:
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql' import { envelop, useEngine } from '@envelop/core' import { useValidationCache } from '@envelop/validation-cache' const getEnveloped = envelop({ plugins: [ useEngine({ parse, validate, specifiedRules, execute, subscribe }), // ... 其他插件 ... useValidationCache({ // 选项 }) ] })插件链中的顺序并不强制要求useValidationCache放在最后,但从 实现 来看,它通过onValidate阶段的setValidationFn包装验证函数,因此它会感知到链中其他插件动态添加的验证规则。
API 参考:cache选项与默认行为
插件暴露的选项非常精简,只有一个cache:
export type ValidationCacheOptions = { cache?: ValidationCache }cache用于传入自定义缓存实例;若不传,插件会创建一个默认的 LRU 缓存。在 实现 中可以看到默认参数:
max: 1000:最多缓存 1000 个验证结果,超出后按 LRU 策略淘汰最久未使用的条目;ttl: 3_600_000(毫秒,即 1 小时):缓存条目存活时长,过期后自动失效。
值得注意的是,这个默认值是当前版本的内部实现细节;在插件的 2.0.0 版本之前,max和ttl曾作为插件选项直接暴露,2.0.0 起(见 CHANGELOG)移除了这两个选项,改为要求用户通过自定义缓存实例来定制容量与过期策略。因此如果你需要调整缓存大小或 TTL,请自己构造缓存实例:
import { LRUCache } from 'lru-cache' import type { GraphQLError } from 'graphql' import { useValidationCache } from '@envelop/validation-cache' const cache = new LRUCache<string, readonly GraphQLError[]>({ max: 5000, // 自定义容量 ttl: 60_000 // 自定义 60 秒过期 }) const plugin = useValidationCache({ cache })自定义缓存只需实现ValidationCache接口——一个带get/set方法的键值存储:
export interface ValidationCache { get(key: string): readonly GraphQLError[] | undefined set(key: string, value: readonly GraphQLError[]): void }这一点也使得该插件可以无缝对接外部存储(如 Redis、内存表等),而不必受限于内置 LRU。
缓存键的构造原理(核心机制)
验证结果能否安全复用,完全取决于缓存键的设计。从 src/index.ts 可以看到,缓存键由三部分拼接而成:
key = schemaHash + '|' + ruleKey + '|' + documentString1. schemaHash:Schema 哈希
const schemaHashCache = new WeakMap<GraphQLSchema, string>() function getSchemaHash(schema: GraphQLSchema) { let hash = schemaHashCache.get(schema) if (hash) return hash const introspection = introspectionFromSchema(schema) hash = String(objectHash(introspection.__schema)) schemaHashCache.set(schema, hash) return hash }验证结果依赖 schema 的具体形态(字段、类型、指令等),因此键中必须包含 schema 身份。实现通过对introspectionFromSchema得到的 introspection 结果进行object-hash哈希,得到一段稳定的 schema 指纹,并用WeakMap缓存哈希结果避免重复计算。
这一设计的演进值得关注:在 5.1.0 之前,插件检测到不同 schema 时是整体重置缓存;5.1.0(见 CHANGELOG)改为将 schema 的 introspection sha1 哈希纳入缓存键,从而在 schema 变化时只失效受影响的部分,而不是清空整个缓存。在 5.1.0 时代该哈希由fast-json-stable-stringify+js-sha1计算;到 10.1.0,底层哈希库由hash-it替换为object-hash(见 CHANGELOG)。
2. ruleKey:验证规则集合
let ruleKey = '' if (Array.isArray(args[2])) { for (const rule of args[2]) { ruleKey = ruleKey + rule.name } }validate的第三个参数是验证规则数组。不同规则组合会得出不同的验证结论,因此规则名拼接串也被纳入键。规则名按数组原始顺序拼接(源码注释也提到可以做排序但认为“可能过度”)。
这个字段是 5.0.5 版本加入的(见 CHANGELOG),用于防止跳过其他插件条件性添加的验证规则。因此该版本起要求:自定义验证规则必须拥有唯一的name属性,否则不同规则可能碰撞出相同的 key 前缀。
3. documentString:操作文档字符串
const key: string = schemaHashKey + `|` + ruleKey + `|` + getDocumentString(params.documentAST, print)操作文档由getDocumentString得到:它优先返回 Envelop 内部documentStringMap(WeakMap)中已记录的原始文档字符串,否则回退到print(document)打印 AST。getDocumentString由@envelop/core导出,定义在 document-string-map.ts,该工具会记忆化结果。
这里的关键改进来自 2.3.0(见 CHANGELOG):改用用户发送的原始文档字符串作为键,而不是打印 AST。原因在于print会把文档规范化(如调整空白与缩进),导致语义相同但书写不同的文档产生不同键,白白降低命中率;而使用原始字符串可以在大多数情况下获得更高的缓存命中率。
源码级流程:onValidate 包装与错误结果缓存
插件只挂载了一个生命周期钩子onValidate,完整流程如下(对应 src/index.ts):
onValidate({ params, setValidationFn, validateFn }) { setValidationFn((...args) => { // 1. 计算 schema 哈希(带 WeakMap 缓存) // 2. 拼接规则名 // 3. 构造 key = schemaHash | ruleKey | documentString const cachedResult = resultCache.get(key) if (cachedResult !== undefined) { return cachedResult // 命中:直接返回,跳过 validate } const result = validateFn(...args) resultCache.set(key, result) // 未命中:执行验证并写入缓存 return result }) }几个值得注意的实现细节:
- 通过
setValidationFn而非直接读取params.rules构造键。源码注释明确指出:插件链中的其他插件可能会在onValidate期间动态追加规则,若在包装函数之外提前固定规则集合,会导致缓存键与实际执行的规则不一致。 - 错误结果同样被缓存。
validate返回的GraphQLError[](包括验证失败的结果)会被原样写入缓存。对应测试用例 “Should call validate once once when operation is cached and errored”(见 validation-cache.spec.ts)验证了:同一个非法操作连续执行两次,底层 validate 只被调用一次,且两次返回结果一致。 - 每次请求只读一次缓存。1.0.1 版本的 Patch(见 CHANGELOG)专门修复了单请求内多次读取缓存的问题。
测试用例:行为契约一览
仓库自带的 validation-cache.spec.ts 使用@envelop/testing的createTestkit和 jest mock 验证了插件的核心行为契约:
| 测试场景 | 断言要点 |
|---|---|
| 缓存为空时 | 执行操作会调用一次真实的 validate |
| 同一操作重复执行 | validate 仅被调用 1 次(缓存命中) |
| 同一非法操作重复执行 | validate 仅被调用 1 次,且两次返回的错误结果相等 |
| 不同操作文档 | 分别触发 validate(key 不同) |
传入ttl: 1的自定义缓存 | 等待 10ms 后再次执行,validate 重新调用(过期失效) |
| 传入自定义 LRU 实例 | 缓存实例的get/set被实际调用 |
动态追加NoSchemaIntrospectionCustomRule | 首次执行__schema查询无错误,第二次因规则集合变化而缓存未命中,重新验证后返回错误——证明规则集合参与键构造 |
| 动态切换 schema | schema1 → schema2 → schema1 过程中,验证次数正确反映缓存键中包含 schema 指纹 |
其中“规则集合参与键构造”与“schema 参与键构造”两个用例,正是缓存键三要素(schemaHash、ruleKey、documentString)的实证支撑。
与 GraphQL Yoga 的内置集成
如果你使用的是 GraphQL Yoga(而非裸 Envelop),验证缓存默认就是开启的,无需手动安装本插件。Yoga 在 use-parser-and-validation-cache.ts 中内置了合并的解析 + 验证缓存插件:
- 默认通过
validationCache = true开启;可通过validationCache: false显式关闭; - 验证缓存基于
rulesKey(规则名按,连接)→WeakMap<GraphQLSchema, WeakMap<DocumentNode, GraphQLError[]>>的嵌套结构组织,即同一规则集合 + 同一 schema + 同一 DocumentNode 才命中缓存,与@envelop/validation-cache的键语义一致,但使用WeakMap而非字符串键,避免长期持有文档对象; - 也支持传入自定义存储:
validationCache选项可接受boolean | Cache<typeof validate>,配合documentCache/errorCache即可替换默认缓存。
完整的关闭与自定义配置方式见官方文档 Parsing and Validation Caching:
import { createServer } from 'node:http' import { createYoga } from 'graphql-yoga' import { schema } from './my-schema' const yoga = createYoga({ schema, parserCache: false, // 关闭解析缓存 validationCache: false // 关闭验证缓存 })import { createServer } from 'node:http' import { DocumentNode, GraphQLError } from 'graphql' import { createYoga } from 'graphql-yoga' import { documentCacheStore, errorCacheStore, validationCacheStore } from './my-cache' import { schema } from './my-schema' interface CacheStore<T> { get(key: string): T | undefined set(key: string, value: T): void } const yoga = createYoga({ schema, parserCache: { documentCache: documentCacheStore as CacheStore<DocumentNode>, errorCache: errorCacheStore as CacheStore<Error> }, validationCache: validationCacheStore as CacheStore<readonly GraphQLError[]> })版本演进时间线:从 CHANGELOG 看设计决策
通过 CHANGELOG.md 可以还原该插件十余个版本的设计演进脉络,理解“为什么现在是这个样子”:
| 版本 | 关键变更 | 设计含义 |
|---|---|---|
| 1.0.1 | 每次请求只读一次缓存 | 修复重复读取开销 |
| 2.0.0 | 移除max/ttl选项,改为传入自定义缓存实例 | 缓存策略与插件解耦 |
| 2.2.0 / 2.1.0 | GraphQL v16 支持 | 跟上上游版本 |
| 2.3.0 | 用原始文档字符串作为缓存键替代 AST 打印 | 提升命中率 |
| 4.5.0 | tiny-lru替换为lru-cache;clear弃用为reset | 统一缓存库与 API |
| 5.0.5 | 规则名纳入缓存键 | 避免跳过条件验证规则,要求规则有唯一name |
| 5.1.0 | schema introspection 哈希纳入缓存键 | 取代“换 schema 即清空缓存”的粗暴策略 |
| 5.1.2 | sha1-es替换js-sha1 | 修复 Edge Runtime 兼容 |
| 5.1.3 | ESM 环境下的验证缓存修复 | 解决 ESM 打包兼容 |
| 6.0.0 | 弃用 Node 14;用WeakMap<DocumentNode>替代字符串 LRU(配合解析缓存);导出getDocumentString | 借助解析缓存实现更优的内存利用 |
| 7.0.0 | 弃用 Node 16 | 跟随运行时基线 |
| 9.0.1 | lru-cache升至^11.0.0 | 依赖升级 |
| 10.1.0 | hash-it替换为object-hash@^3.0.0 | 哈希库替换 |
| 10.2.0 | 支持 graphql-js 17,适配subscribe的类型 | 保持对最新 graphql 的兼容 |
| 10.2.1 | package.json 补充 homepage 与 bugs 字段 | 元数据完善 |
其中 6.0.0 的变更尤其值得展开(见 CHANGELOG):当配合@envelop/parser-cache(源码)使用时,解析结果DocumentNode本身已被缓存,此时再用字符串 LRU 缓存验证结果会产生重复的文档引用;改为WeakMap<DocumentNode>后,文档生命周期由解析缓存管理,验证缓存不会持有额外引用,内存更优。
实践建议与注意事项
综合源码、测试与演进历史,在实际使用中请注意以下几点:
- 自定义验证规则必须设置唯一的
name:规则名是缓存键的一部分,重名规则会导致键冲突。 - 不要依赖默认
max/ttl做容量控制:调整容量与过期时间请传入自定义 LRU 实例(默认 1000 条 / 1 小时仅适用于大多数场景)。 - 建议与
useParserCache配合使用:解析缓存能让getDocumentString优先命中原始文档字符串(而非print),进一步提高键的一致性与命中率。 - 动态 schema / 动态规则场景无需额外处理:schema 哈希与规则名都参与键构造,schema 或规则变化时相关条目自然失效。
- Edge Runtime 与 ESM 支持均已就绪:5.1.2 / 5.1.3 起修复了边缘运行时与 ESM 环境问题,当前版本可以放心用于 Cloudflare Workers、Deno 等环境。
- 使用 GraphQL Yoga 时无需重复安装:Yoga 默认内置验证缓存,直接通过
validationCache: false或自定义 store 控制即可。
总结
@envelop/validation-cache通过“schema 哈希 + 规则名集合 + 操作文档字符串”三要素构造缓存键,以 LRU 缓存安全地复用validate的结果(包括错误结果),为 GraphQL 服务带来约 50% 的验证性能提升。它既可作为独立 Envelop 插件接入,也已默认内置在 GraphQL Yoga 中;其十余个版本围绕命中率、内存效率、运行时兼容的持续演进,本身也是一份值得借鉴的缓存插件设计范本。深入源码路径 src/index.ts、测试 与 CHANGELOG 可以进一步验证上述全部结论。
- 后端
- API设计
【免费下载链接】graphql-yoga
🧘 Rewrite of a fully-featured GraphQL Server with focus on easy setup, performance & great developer experience. The core of Yoga implements WHATWG Fetch API and can run/deploy on any JS environment.
相关推荐
@envelop/parser-cache 插件实战:用 LRU 缓存为 GraphQL Yoga 的 parse 阶段提速约 60%
@envelop/parser cache 插件实战:用 LRU 缓存为 GraphQL Yoga 的 parse 阶段提速约 60% GraphQL 请求处理
后端API设计Envelop Extended Validation 实战:在 graphql-yoga 中编写可访问 Variables 的 GraphQL 验证规则
Envelop Extended Validation 实战:在 graphql yoga 中编写可访问 Variables 的 GraphQL 验证规则 本文
后端API设计@envelop/validation-cache 使用指南:为 GraphQL 校验层引入 LRU 缓存,将 validation 性能提升约 50%
@envelop/validation cache 使用指南:为 GraphQL 校验层引入 LRU 缓存,将 validation 性能提升约 50% 本文围
后端API设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考