资讯详情

资讯详情

graphql-ruby 分页 Cursor 完全指南:不透明游标的原理、默认编码与自定义 `cursor_encoder`

后端API设计【免费下载链接】graphql-rubyRuby implementation of GraphQL项目地址https://gitcode.com/gh_mirrors/gr/graphql-ruby点击查看免费下载游标Cursor是 graphql-ruby 中 Relay 风格 Connection 分页的核心机制它以一种不透明的字符串标记列表中的具体位置客户端无需也无法解读其内部含义只需在拿到游标后将其回传给after/before参数即可继续向前或向后翻页。本文以 guides/pagination/cursors.md 为主线结合 lib/graphql/pagination/connection.rb、lib/graphql/schema.rb 与 lib/graphql/schema/base_64_encoder.rb 等源码实现为你讲清游标的工作方式、默认 Base64 编码细节以及如何通过Schema.cursor_encoder接入自定义编码器并给出可直接复制的完整代码示例。什么是 Cursor连接分页的位置标记在 Connection 分页模型中客户端通过四个参数控制翻页first、last、after、before。其中after和before接收的字符串就是cursor游标它指示列表中某一个具体的位置点。游标由连接实现生成并返回给客户端通常位于edges { cursor }、pageInfo { startCursor endCursor }中客户端再次请求时把游标原样传回服务端据此确定当前页的起点或终点。在 graphql-ruby 中连接字段由ConnectionExtension自动注入after、before、first、last四个参数见 lib/graphql/schema/field/connection_extension.rb。游标之所以设计成**不透明opaque**字符串是为了把分页实现细节完全封装在服务端不可解码客户端不应尝试 base64 解码、逆向工程或猜解游标中隐含的信息例如第 N 条不可临时拼造客户端不应根据业务逻辑自己构造游标唯一保证拿到一个游标后可以放心地用它请求该位置之后after或之前before的列表项。这种值本身无意义、只有回传才有意义的契约让服务端可以自由选择最高效、最稳定的底层分页策略——这正是游标被 Relay 风格连接采用的原因。如 guides/pagination/connection_concepts.md 所述连接分页相比传统的 offset 分页优势之一就是游标实现可以支持高效且稳定的分页。游标底层是什么从编码到偏移量的完整链路要理解不透明最好的办法是看它透明的一面——即服务端内部是如何使用游标的。以最常用的ArrayConnection为例lib/graphql/pagination/array_connection.rb 的实现非常直白# 生成游标取 item 在数组中的下标 1编码成字符串 def cursor_for(item) idx items.find_index(item) 1 encode(idx.to_s) end # 解析游标解码字符串得到正整数偏移量 def index_from_cursor(cursor) index Integer(decode(cursor), 10, exception: false) if index.nil? || index 0 raise GraphQL::ExecutionError, Invalid cursor: #{cursor.inspect} end [index, items.length 1].min end也就是说游标的明文就是列表中的 1-based 位置索引1、2、3……经过编码后对外呈现为一串无意义的字符。而RelationConnectionActiveRecord / Sequel / Mongoid 连接的基础类则把游标解码为 SQLOFFSET见 lib/graphql/pagination/relation_connection.rbdef offset_from_cursor(cursor) offset Integer(decode(cursor), 10, exception: false) if offset.nil? || offset 0 raise GraphQL::ExecutionError, Invalid cursor: #{cursor.inspect} end offset end这里有两个值得注意的工程细节解码失败即报错decode抛出的非法字符、Integer()无法解析的值、 0的游标都会统一转换为GraphQL::ExecutionError错误消息Invalid cursor: ...而不是静默返回空页。测试 spec/support/connection_assertions.rb 还验证了负数游标不会回绕到列表尾部、超出列表长度的游标返回空结果、第一个游标之前的before查询返回空列表等边界行为。同一个游标用于双向翻页before与after共用同一套偏移量语义因此游标天然支持向前before/last与向后after/first双向分页。整个链路可以概括为列表位置 → 明文偏移量字符串 → 编码器.encode→ 不透明游标对外→ 客户端回传 → 编码器.decode→ 偏移量 → 切片/SQL OFFSET。encode与decode正是连接实例上可供调用的两个方法见下文自定义 Cursor 编码器。默认编码器无填充的 URL 安全 Base64graphql-ruby 的默认游标编码器是GraphQL::Schema::Base64Encoder定义在 lib/graphql/schema/base_64_encoder.rbmodule Base64Encoder def self.encode(unencoded_text, nonce: false) Base64.urlsafe_encode64(unencoded_text, padding: false) end def self.decode(encoded_text, nonce: false) # urlsafe_decode64 is for forward compatibility Base64.urlsafe_decode64(encoded_text) rescue ArgumentError raise GraphQL::ExecutionError, Invalid input: #{encoded_text.inspect} end end可以看到默认实现有两个关键选择URL 安全urlsafe使用Base64.urlsafe_encode64把标准 Base64 中的/替换为-_避免游标作为查询字符串或 JSON 传输时产生转义歧义无填充padding: false去掉末尾的让游标更紧凑解码容错解码遇到非法输入时抛出GraphQL::ExecutionError而不是底层库异常保证错误以 GraphQL 错误形式呈现给客户端。Base64Encoder是游标编码的出厂默认值在 lib/graphql/schema.rb 中cursor_encoder未配置时通过find_inherited_value(:cursor_encoder, Base64Encoder)回退到该模块并且该配置支持沿 Schema 继承链传递spec/graphql/schema_spec.rb 中验证了子类 Schema 会继承父类的cursor_encoder。自定义 Cursor 编码器Schema.cursor_encoder如果默认的 Base64 编码不满足需求例如需要更短、加密、或与既有系统对齐的游标格式可以在 Schema 上通过cursor_encoder指定自定义编码器。编码器接口契约自定义编码器只需是一个响应两个类方法或实例方法的对象.encode(plain_text, nonce: false) # 返回编码后的字符串 .decode(encoded_text, nonce: false) # 返回解码后的明文方法签名里的nonce:关键字参数是游标编码的扩展点连接在内部调用编码器时总是以nonce: true传递见 lib/graphql/pagination/connection.rbdef decode(cursor) context.schema.cursor_encoder.decode(cursor, nonce: true) end def encode(cursor) context.schema.cursor_encoder.encode(cursor, nonce: true) end也就是说编码器可以通过nonce: true区分内部生成的游标与其他场景下的调用例如校验客户端传入的游标是否由本服务端签发从而为游标附加一次性随机数nonce或签名进一步增强防篡改能力。默认的Base64Encoder直接忽略nonce参数。官方示例URL 安全 Base64沿用官方文档的完整示例将 Schema 的游标编码切换到 URL 安全 Base64module URLSafeBase64Encoder def self.encode(txt, nonce: false) Base64.urlsafe_encode64(txt) end def self.decode(txt, nonce: false) Base64.urlsafe_decode64(txt) end end class MySchema GraphQL::Schema # ... cursor_encoder(URLSafeBase64Encoder) end配置完成后该 Schema 下所有连接生成的游标都会使用这个编码器包括内置的ArrayConnection、ActiveRecordRelationConnection、SequelDatasetConnection、MongoidRelationConnection以及任何自定义连接包装器——因为它们统一经由基类GraphQL::Pagination::Connection的encode/decode方法访问context.schema.cursor_encoder见 lib/graphql/pagination/connection.rb。测试中的编码器形态认识nonce的真实用途仓库测试 spec/support/connection_assertions.rb 提供了一个值得参考的NonceEnabledEncoder它演示了 nonce 参数在实践中的用法class NonceEnabledEncoder class self def encode(value, nonce: false) #{JSON.dump([value])}#{nonce ? nonce : } end def decode(value, nonce: false) if nonce value value.sub(/\nonce$/, ) end JSON.parse(value).first end end end该编码器在nonce: true即 graphql-ruby 内部调用时给明文附加nonce标记并在解码时剥离。测试 spec/graphql/pagination/connection_spec.rb 用它对[-1, 0, abc, 1e10]这类畸形游标做了验证确认都会被拒绝并抛出包含Invalid cursor的GraphQL::ExecutionError。由此可以推断你可以利用nonce:参数给游标加盐、加签名或附加过期时间让不透明升级为不可伪造。自定义连接中如何使用encode/decodeGraphQL::Pagination::Connection的子类在实现分页时可以直接调用继承自基类的encode与decode私有方法无需自己持有编码器引用。例如 lib/graphql/pagination/array_connection.rb 与 lib/graphql/pagination/relation_connection.rb 生成游标时都是encode(offset.to_s)的写法。如果你在编写自己的连接包装器参见 guides/pagination/custom_connections.md 中自定义连接的要求实现#cursor_for(item)时只需class SearchResultsConnection GraphQL::Pagination::Connection def cursor_for(item) # 计算出 item 在列表中的稳定位置交给 encode 处理 encode(position_of(item).to_s) end def nodes # 用 decode 还原客户端回传的 before/after 位置 start_pos after Integer(decode(after), 10) # ... 基于位置切片 end end这样一来你的自定义连接会自动遵守当前 Schema 配置的编码器与内置连接行为保持一致。实战注意事项与边界行为结合源码与测试在实际使用游标分页时应当留意以下几点不要在前端解析游标游标格式包括默认 Base64不属于稳定 API升级 graphql-ruby 版本后可能变化客户端应把游标当作黑盒字符串原样存储、原样回传。空字符串游标按nil处理lib/graphql/pagination/connection.rb 中before/after的会被视为nil避免空字符串引发解码错误。非法游标会报错而非静默解码失败、非正整数偏移如-1、0、abc、1e10都会抛出GraphQL::ExecutionError消息Invalid cursor并遵守max_page_size限制不会绕过分页上限见 spec/support/connection_assertions.rb。越界游标返回空列表游标对应的位置超出列表范围时返回空结果但不会把负数回绕到列表尾部。双向分页天然可用同一个游标既能作为after向后翻页也能作为before向前翻页hasNextPage/hasPreviousPage由连接根据游标位置自动计算。延伸阅读guides/pagination/connection_concepts.mdConnection / Edge / Node 的概念与关系元数据guides/pagination/using_connections.md如何定义连接字段、返回集合以及配置max_page_size/default_page_sizeguides/pagination/custom_connections.md如何为自有数据对象实现连接包装器必须实现#cursor_for等四个方法lib/graphql/pagination/connection.rb连接基类encode/decode与cursor_for的声明所在lib/graphql/schema/base_64_encoder.rb默认游标编码器的完整实现。赞分享后端API设计【免费下载链接】graphql-rubyRuby implementation of GraphQL项目地址https://gitcode.com/gh_mirrors/gr/graphql-ruby点击查看免费下载相关推荐Keystone 6 bytes 字段自定义编码实战从 Hex 默认值到 Base64 自定义 GraphQL 标量Keystone 6 bytes 字段自定义编码实战从 Hex 默认值到 Base64 自定义 GraphQL 标量 本文以仓库中 examples/byte后端FlexSearch Encoder 完全指南从默认配置到自定义编码管线FlexSearch Encoder 完全指南从默认配置到自定义编码管线 导读 Encoder编码器是 FlexSearch 中负责语言处理与模糊性搜索引擎后端LNReader完整指南打造终极Android轻小说阅读体验的免费开源方案LNReader完整指南打造终极Android轻小说阅读体验的免费开源方案 LNReader是一款专为Android平台设计的开源轻小说阅读器通过强大的插件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →