本篇指南围绕 Relay Classic 时代的核心概念Routes(路由)展开:Relay 的路由负责声明一个应用的查询入口(query roots),是连接 GraphQL 根查询与可复用片段(fragments)的桥梁。读完本文,你将掌握查询与片段的本质区别、如何用对象字面量与Relay.Route定义可复用的路由、如何通过Relay.RootContainer将路由与容器组合成完整数据请求,以及这一机制在现代 Relay 中如何演化为 EntryPoint 体系。
为什么需要 Routes:先理解 Queries 与 Fragments 的本质区别
要理解 Relay 为什么需要路由,必须先厘清 GraphQL 中**查询(queries)与片段(fragments)**这两种声明方式的差异——这是 Classic-Guides-Routes.md 全篇的逻辑起点。
Queries:声明根查询类型上的字段
在 GraphQL 中,query声明的是存在于根查询类型(root query type)上的字段。例如,下面的查询会获取id为123的用户名称:
query UserQuery { user(id: "123") { name, }, }查询锚定在根类型上,天然带有"入口"属性:一条查询就是一次从应用顶部发起的完整数据请求。
Fragments:声明任意类型上的字段
与之相对,GraphQLfragment声明的是任意类型上的一组字段。例如,下面的片段为"某个User"获取头像 URI:
fragment UserProfilePhoto on User { profilePhoto(size: $size) { uri, }, }片段不关心自己被用在哪棵数据树上,因此它可以被嵌入到其他片段或查询之中。上面的片段既可用于获取用户123的头像:
query UserQuery { user(id: "123") { ...UserProfilePhoto, }, }也可以用于批量获取用户123所有好友的头像:
query UserQuery { user(id: "123") { friends(first: 10) { edges { node { ...UserProfilePhoto, }, }, }, }, }为什么容器要声明片段而非查询
Relay 容器(Container)声明的是片段而非查询,这使它们可以被轻易嵌入到多个上下文中。正如 Classic-Guides-Containers.md 所述,容器是描述数据需求规格的高阶组件——Relay.createContainer(Component, { fragments: {...} })接收一个 React 组件并返回一个能够自动获取数据的新组件。因为片段与具体入口解耦,同一个容器可以出现在列表页、详情页、弹窗等不同位置,这与 React 组件天然可复用、可组合的特性完全一致。
| 维度 | Query | Fragment |
|---|---|---|
| 锚定位置 | 根查询类型 | 任意类型 |
| 是否可嵌入 | 可作为片段宿主 | 可嵌入其他片段/查询 |
| 复用粒度 | 一次请求一个入口 | 跨上下文任意复用 |
| 在 Relay 中的角色 | 由 Route 声明 | 由 Container 声明 |
定义路由:对象字面量与 Relay.Route 子类
路由(Route)是定义了根查询集合与输入参数的对象。Relay 需要知道"从哪个根查询出发"才能把容器声明的片段组装成一次完整的 GraphQL 请求,路由正是承担这一职责的入口声明。
对象字面量:最简单的路由
这里是一个用于渲染用户123个人资料页的简单路由:
var profileRoute = { queries: { // Routes declare queries using functions that return a query root. Relay // will automatically compose the `user` fragment from the Relay container // paired with this route on a Relay.RootContainer user: () => Relay.QL` # In Relay, the GraphQL query name can be optionally omitted. query { user(id: $userID) } `, }, params: { // This `userID` parameter will populate the `$userID` variable above. userID: '123', }, // Routes must also define a string name. name: 'ProfileRoute', };三个关键点:
queries:以"函数返回Relay.QL查询根"的形式声明。函数形式是刻意的——它允许变量在实例化时才被注入,而不是在模块加载时固定。params:提供查询所需的变量值,这里的userID: '123'会填充查询模板里的$userID变量。name:路由必须定义一个字符串名称,用于调试与标识。
子类化 Relay.Route:参数化复用
对象字面量只能服务固定参数。要为任意用户创建路由实例,可以子类化Relay.Route抽象类,它使"定义一组查询与必需参数并可多次复用"变得容易:
class ProfileRoute extends Relay.Route { static queries = { user: () => Relay.QL` query { user(id: $userID) } `, }; static paramDefinitions = { // By setting `required` to true, `ProfileRoute` will throw if a `userID` // is not supplied when instantiated. userID: {required: true}, }; static routeName = 'ProfileRoute'; }现在可以实例化一个获取用户123数据的ProfileRoute:
// Equivalent to the object literal we created above. var profileRoute = new ProfileRoute({userID: '123'});并且可以针对任意用户 ID 创建路由。例如,在监听popstate事件、从 URI 读取userID查询参数后构造路由并渲染:
window.addEventListener('popstate', () => { var userID = getQueryParamFromURI('userID', document.location.href); var profileRoute = new ProfileRoute({userID: userID}); ReactDOM.render( <Relay.RootContainer Component={UserProfile} route={profileRoute} />, document.getElementById('app') ); });这段代码也体现了原文档的一个重要澄清:Relay 路由并不实现任何 URL 路由逻辑,也不与 History API 协作。作者在文档中直言未来可能将RelayRoute改名为类似RelayQueryRoots或RelayQueryConfig的名字。URL 的解析、监听与切换应交给专门的 router 库完成,Relay 路由只关心"这次渲染需要从哪些查询根取数"。这里Component={UserProfile}与route={profileRoute}的配对即是在Relay.RootContainer上进行的,详见下文。
Relay.Route 完整 API 参考
Classic-APIReference-Route.md 对Relay.Route给出了完整定义。路由类共包含 4 个静态属性与 1 个构造方法。
static paramDefinitions
static paramDefinitions: {[param: string]: {required: boolean}}路由可以声明一组必须在构造时提供的参数名,同时这也是记录合法参数集合的好地方。若在实例化时缺少标为required: true的参数,会直接抛错:
class ProfileRoute extends Relay.Route { static paramDefinitions = { userID: {required: true}, }; // ... }static prepareParams
static prepareParams: ?(prevParams: {[prevParam: string]: mixed}) => {[param: string]: mixed};prepareParams用于提供默认参数,或对传入参数进行透传、转换与抑制。它是路由可复用性的进阶手段——例如把业务 ID 转换为全局 ID(toGlobalId)、或为查询注入默认的limit变量:
class ProfileRoute extends Relay.Route { static queries = { viewer: () => Relay.QL`query { viewer }` }; static prepareParams = (prevParams) => { return { // Pass base set of supplied params through: ...prevParams, // Transform a param to meet internal requirements: id: toGlobalId('Profile', prevParams.id), // Provide a starting `limit` variable: limit: 10, } } // ... }static queries
static queries: { [queryName: string]: () => Relay.QL`query { ... }` };路由必须用Relay.QL声明一组查询根。这些查询会在Relay.RootContainer上自动组合同名容器片段:即路由queries.user会自动与容器的user片段合并成完整请求:
class ProfileRoute extends Relay.Route { static queries = { user: () => Relay.QL`query { user(id: $userID) }`, }; // ... }在这个例子中,路由需要用userID初始化,该值会被传入查询。$userID变量会自动下传给顶层容器并在需要时被使用;同时,顶层 Relay 容器应有一个包含所需字段的user片段。
static routeName
static routeName: string路由必须定义一个字符串名称。该名称用于标识请求、调试与日志,是路由的"身份"。
constructor(initialParams)
使用new关键字创建路由实例,可传入初始参数:
var profileRoute = new ProfileRoute({userID: '123'});若paramDefinitions中声明了必填参数而构造时未提供,构造过程会抛错;prepareParams会在参数进入查询之前被应用。
用 Relay.RootContainer 组装查询并渲染
路由解决了"声明查询根"的问题,容器解决了"组件声明片段"的问题。要把二者组合成一条可发送给服务器的完整 GraphQL 查询,需要 Classic-Guides-RootContainer.md 中的Relay.RootContainer。
组件与路由的配对
Relay.RootContainer是一个 React 组件,给定Component与route,它会尽力满足渲染该组件实例所需的数据:
ReactDOM.render( <Relay.RootContainer Component={ProfilePicture} route={profileRoute} />, container );渲染时,Relay 会构造查询并发送给 GraphQL 服务器;当所有必需数据获取完成后,ProfilePicture才会被渲染,其携带片段的 props 中包含来自服务器的数据。若Component或route任一发生变化,Relay.RootContainer会立即开始满足新的数据需求——这正是"路由作为入口"在运行时层面的体现:切换路由即切换入口与数据需求。
渲染回调:renderLoading / renderFetched / renderFailure
Relay.RootContainer接受三个可选回调 props,用于精细控制渲染行为。
renderLoading:每当无法立即满足渲染所需数据时(通常发生在首次渲染,或Component/route变化时)触发。默认情况下,首次加载时什么都不渲染;若此前已渲染过一组Component/route,默认行为是继续渲染旧视图。提供renderLoading可覆盖:
<Relay.RootContainer Component={ProfilePicture} route={profileRoute} renderLoading={function() { return <div>Loading...</div>; }} />注意语义细节:renderLoading返回undefined等价于默认行为(继续展示旧视图);返回null则无论是否有旧视图都渲染空。
renderFetched:当渲染所需数据全部可用时触发。回调总是带一个data参数,它是从propName到查询数据的映射,通常配合 JSX 展开属性使用:
<Relay.RootContainer Component={ProfilePicture} route={profileRoute} renderFetched={function(data) { return ( <ScrollView> <ProfilePicture {...data} /> </ScrollView> ); }} />文档特别提醒:尽管可以访问data对象,但它刻意保持不透明,防止renderFetched对Component声明的片段产生隐式依赖。
renderFailure:当发生阻止数据获取的错误时触发,默认不渲染任何内容。回调接收error(Error 对象)与retry(重试函数)两个参数;若错误来自服务器响应,响应负载可通过error.source检查:
<Relay.RootContainer Component={ProfilePicture} route={profileRoute} renderFailure={function(error, retry) { return ( <div> <p>{error.message}</p> <p><button onClick={retry}>Retry?</button></p> </div> ); }} />强制取数:forceFetch
与 Relay 大多数 API 一致,Relay.RootContainer会先尝试从客户端 store 解析数据,失败才发服务器请求。若希望即使客户端已有数据也强制请求服务器,可使用forceFetch布尔 prop:
<Relay.RootContainer Component={ProfilePicture} route={profileRoute} forceFetch={true} />forceFetch为 true 时,只要渲染所需数据在客户端可用,renderFetched仍可能在服务器请求完成前被调用;此时回调会收到第二个参数readyState,其stale属性为 true 表示"数据来自客户端缓存、服务器刷新尚未完成":
<Relay.RootContainer Component={ProfilePicture} route={profileRoute} forceFetch={true} renderFetched={function(data, readyState) { var isRefreshing = readyState.stale; return ( <ScrollView> <Spinner style={{display: isRefreshing ? 'block' : 'none' }} <ProfilePicture {...data} /> </ScrollView> ); }} />这为"后台刷新 + 即时展示缓存"的交互提供了标准范式。
就绪状态:onReadyStateChange
Relay.RootContainer还支持onReadyStateChangeprop,用于在满足数据需求的过程中接收细粒度事件。该回调会被调用一次或多次,每次传入描述当前"就绪状态"的对象,属性包括:
ready: boolean—— 渲染所需的数据子集是否就绪done: boolean—— 所有数据需求是否全部就绪error: ?Error—— 失败时为Error实例,否则为nullevents: Array<ReadyStateEvent>—— 截至目前收到的事件数组stale: boolean—— 强制取数时,若ready因客户端缓存数据为 true 而服务器请求未完成,则为 trueaborted: boolean—— 请求是否被中止
ReadyStateEvent枚举包括ABORT、CACHE_RESTORED_REQUIRED、CACHE_RESTORE_FAILED、CACHE_RESTORE_START、NETWORK_QUERY_ERROR、NETWORK_QUERY_RECEIVED_ALL、NETWORK_QUERY_RECEIVED_REQUIRED、NETWORK_QUERY_START、STORE_FOUND_ALL、STORE_FOUND_REQUIRED。
结合 Classic-Guides-ReadyState.md,可以推演典型调用序列:
- 服务器取数:客户端数据不足触发请求时,先调用一次
ready: false,再调用一次ready与done均为 true。 - 客户端命中:客户端数据充足、无需请求时,只调用一次
ready与done均为 true。 - 服务器错误:先调用一次
ready: false,再调用一次带error的回调,且ready/done持续为 false。 - 强制取数且客户端可渲染:先调用一次
ready、done、stale均为 true,再调用一次ready、done为 true 而stale为 false。
这些时序契约让开发者可以精确记录数据就绪耗时、上报错误或实现加载态切换。
Relay.QL:路由查询的语法基础
路由中的查询根都以Relay.QL标签模板声明,因此理解其编译机制是掌握路由的底层前提。Classic-APIReference-QL.md 指出:Relay 的片段、变更与查询必须使用以Relay.QL标记的 ES6 模板字符串指定,例如:
var fragment = Relay.QL` fragment on User { name } `;由于 GraphQL schema 体积过大、不宜打包进应用,这些Relay.QL模板表达式需要被转译为 JavaScript 描述——这一工作由babel-plugin-relay在编译期完成(当前仓库中的实现见 packages/babel-plugin-relay/BabelPluginRelay.js)。转译后的 schema 信息让 Relay 能理解字段参数类型、哪些字段是连接(connection)或列表、以及如何高效地重新获取(refetch)服务器记录。
Relay.QL对象被四类 API 使用,构成完整的数据声明体系:
| API | 形态 | 用途 |
|---|---|---|
Relay.Container | () => Relay.QL\fragment on ...`` | 声明容器数据依赖(片段) |
Relay.Route | () => Relay.QL\query ...`` | 声明路由查询根 |
Relay.Mutation | Relay.QL\mutation { fieldName }`` | 声明变更字段 |
| 可复用片段 | var fragment = Relay.QL\fragment on ...`;` | 在上述场景中组合复用 |
路由查询中的$userID变量正是通过params/构造参数与prepareParams注入的,变量会在编译期由 babel-plugin 校验与登记,运行期由 Relay 组合进最终请求。
从 Classic Routes 到现代 Relay:同一思想的演进
需要说明的是,当前仓库的packages目录下已不再包含 Classic 时代的Relay.Route运行时实现——本指南所述的 API 以 website/versioned_docs/version-classic 下的版本化文档为准。但这套"入口(root)与片段(fragment)分离、由运行时组合请求"的设计思想一直延续至今,只是入口的载体发生了演化:
Classic 时代:入口 =
Relay.Route+Relay.RootContainer,通过new ProfileRoute({userID})与Componentprop 配对完成数据满足。现代 Relay:入口演化为EntryPoint 体系(见 website/versioned_docs/version-v21.0.1/api-reference/entrypoint-apis/entrypoint-container.mdx),配合
QueryRenderer及 hooks 形态的useLazyLoadQuery(源码见 packages/react-relay/relay-hooks/useLazyLoadQuery.js)、loadQuery(packages/react-relay/relay-hooks/loadQuery.js)与useEntryPointLoader等 API。现代查询同样以"声明 root query + 变量"为入口,只是换成了graphql标签与预加载(preload)模型。编译管线:Classic 依赖 Babel 插件转译
Relay.QL;现代版本则可在编译期借助仓库中的 Rust 编译器(compiler/crates/relay-compiler)与babel-plugin-relay(packages/babel-plugin-relay)生成持久化的请求元数据。
理解 Classic 的 Routes,等于理解了 Relay 数据声明的"最小完备单元":查询根负责入口、片段负责复用、参数负责实例化、RootContainer 负责组装与满足。这套心智模型在阅读现代 EntryPoint 与useLazyLoadQuery文档时依然直接适用。
小结
- 查询 vs 片段:查询锚定根类型、一次一入口;片段锚定任意类型、可任意嵌入复用,容器因此获得与 React 组件一致的可组合性。
- 路由的定义:可用对象字面量(
queries+params+name)快速声明,也可子类化Relay.Route(queries+paramDefinitions+prepareParams+routeName+ 构造器)获得参数化复用与必填校验。 - 路由的使用:与容器配对交给
Relay.RootContainer,由它组装查询、满足数据,并通过renderLoading/renderFetched/renderFailure/forceFetch/onReadyStateChange控制渲染与生命周期。 - 底层机制:所有查询与片段经
Relay.QL声明、由 babel-plugin-relay 在编译期转译;prepareParams与变量注入是参数从 URL/实例到 GraphQL 变量的完整通道。
进一步阅读:路由 API 细节见 Classic-APIReference-Route.md,容器与片段组合见 Classic-Guides-Containers.md,RootContainer 渲染控制与就绪状态见 Classic-Guides-RootContainer.md 与 Classic-Guides-ReadyState.md。
- 前端
- 开发工具
【免费下载链接】relay
Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay
相关推荐
Vue Router 命名路由(Named Routes)完全指南:定义、链接与底层实现
Vue Router 命名路由(Named Routes)完全指南:定义、链接与底层实现 命名路由(Named Routes)是 Vue Router 为每条路
前端路由gpt-oss-20b-WFP8-AFP8-KVFP8量化参数详解:权重、激活和KV缓存配置
gpt oss 20b WFP8 AFP8 KVFP8量化参数详解:权重、激活和KV缓存配置 gpt oss 20b WFP8 AFP8 KVFP8是一个经过全
前端路由Angular 路由定义完全指南:从 Routes 数组到嵌套视图的配置详解
Angular 路由定义完全指南:从 Routes 数组到嵌套视图的配置详解 导读 本文以 Angular 官方仓库中的 Define Routes 开发技能文
前端Web框架