Relay 路由(Routes)完全指南:定义查询入口、组合片段与接入 RootContainer
2026/9/21 1:25:43 网站建设 项目流程

本篇指南围绕 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)上的字段。例如,下面的查询会获取id123的用户名称:

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 组件天然可复用、可组合的特性完全一致。

维度QueryFragment
锚定位置根查询类型任意类型
是否可嵌入可作为片段宿主可嵌入其他片段/查询
复用粒度一次请求一个入口跨上下文任意复用
在 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改名为类似RelayQueryRootsRelayQueryConfig的名字。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 组件,给定Componentroute,它会尽力满足渲染该组件实例所需的数据

ReactDOM.render( <Relay.RootContainer Component={ProfilePicture} route={profileRoute} />, container );

渲染时,Relay 会构造查询并发送给 GraphQL 服务器;当所有必需数据获取完成后,ProfilePicture才会被渲染,其携带片段的 props 中包含来自服务器的数据。若Componentroute任一发生变化,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对象,但它刻意保持不透明,防止renderFetchedComponent声明的片段产生隐式依赖。

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实例,否则为null
  • events: Array<ReadyStateEvent>—— 截至目前收到的事件数组
  • stale: boolean—— 强制取数时,若ready因客户端缓存数据为 true 而服务器请求未完成,则为 true
  • aborted: boolean—— 请求是否被中止

ReadyStateEvent枚举包括ABORTCACHE_RESTORED_REQUIREDCACHE_RESTORE_FAILEDCACHE_RESTORE_STARTNETWORK_QUERY_ERRORNETWORK_QUERY_RECEIVED_ALLNETWORK_QUERY_RECEIVED_REQUIREDNETWORK_QUERY_STARTSTORE_FOUND_ALLSTORE_FOUND_REQUIRED

结合 Classic-Guides-ReadyState.md,可以推演典型调用序列:

  • 服务器取数:客户端数据不足触发请求时,先调用一次ready: false,再调用一次readydone均为 true。
  • 客户端命中:客户端数据充足、无需请求时,只调用一次readydone均为 true。
  • 服务器错误:先调用一次ready: false,再调用一次带error的回调,且ready/done持续为 false。
  • 强制取数且客户端可渲染:先调用一次readydonestale均为 true,再调用一次readydone为 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.MutationRelay.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.Routequeries+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

点击查看免费下载
上一篇:Manta发票应用终极部署指南:三大平台打包发布完整流程
下一篇:GHelper:免费开源的华硕笔记本硬件控制完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询