• 前端
  • 开发工具

【免费下载链接】relay

Relay is a JavaScript framework for building data-driven React applications.

项目地址: https://gitcode.com/gh_mirrors/relay29/relay
点击查看 免费下载

导读

本文围绕 Relay 官方引导教程中的 Client-only data 展开,系统讲解如何通过 Client Schema Extensions(客户端 Schema 扩展) 在浏览器端为 GraphQL 数据模型补充、甚至完整建模纯客户端状态。你将掌握:如何在 .graphql 文件中扩展已有服务端类型或定义全新客户端类型,如何在 Fragment / Query 中像普通字段一样读取这些数据,以及如何借助 commitLocalUpdate、mutation/subscription updater 等 API 更新它们,并了解编译器在底层如何识别、转换与校验这些客户端扩展字段。


什么是 Client-Only Data(客户端 Schema 扩展)

Relay 提供了一种能力:在客户端(浏览器端)扩展 GraphQL Schema,即通过 "client schema extensions" 来建模那些只需要在客户端被创建、读取和更新的数据。这一机制的典型用途有两类:

  1. 为服务端获取的数据补充少量客户端信息——例如给某条评论标记"是否是新评论",用于决定渲染样式;
  2. 完整建模客户端特有的状态——由 Relay Store 统一存储与管理,使 Relay 成为应用内所有数据(含本地数据)的唯一数据源。

从能力范围看,client schema extensions 允许你:

  • 修改已有类型:通过 extend 关键字为 Schema 中已有类型(如 Comment、User、Item)新增字段;
  • 创建全新类型:定义仅存在于客户端的 type、enum 等新类型,甚至让新类型引用服务端已有类型,或让服务端类型引用新的客户端类型。

补充说明:关于"以 Relay 作为客户端全部数据唯一数据源"的完整视角,可参见同版本引导教程的 Local Data Updates;关于更新操作的更多细节可参考 Client Schema Extensions 指南。


扩展已有类型(Extending Existing Types)

在开源(OSS)版本中,要扩展已有类型,只需在你的 --src 源码目录下添加一个 .graphql 文件即可。例如新建一个文件并在其中写入:

extend type Comment {
  is_new_comment: Boolean
}

要点说明:

  • 这里使用 GraphQL 标准 extend 关键字,为已有的 Comment 类型新增字段 is_new_comment: Boolean;
  • 新增字段后,就可以在组件里通过 Fragment 或 Query 正常读取(见下文"读取"一节),并在需要时用常规 Relay API 更新(见下文"更新"一节);
  • 典型业务场景:当用户创建一条新评论时,通过该字段为评论设置"is_new"标记,从而在界面上渲染不同的视觉效果。

从编译器实现看,扩展字段在 Schema 中会被标记为 extension 属性。例如 client_extensions.rs 中通过 schema.object(...).is_extension 等判断来区分扩展类型与服务端类型,build_schema.rs 中则通过 parse_schema_with_extensions_parallel 将扩展 SDL 文档与服务端 Schema 合并解析。这些机制保证了扩展字段能够被后续的校验、转换与代码生成流程正确处理。


添加全新客户端类型(Adding New Types)

你可以用与普通 GraphQL 完全相同的语法定义全新类型,把它们写进 .graphql 文件即可(多个类型可以放在同一个文件中)。示例:

# You can define more than one type in a single file
enum FetchStatus {
  FETCHED
  PENDING
  ERRORED
}

type FetchState {
  # You can reuse client types to define other types
  status: FetchStatus

  # You can also reference regular server types
  started_by: User!
}

extend type Item {
  # You can extend server types with client-only types
  fetch_state: FetchState
}

这个示例演示了客户端 Schema 扩展的几个关键特性:

  1. 定义新 enum 与 type:FetchStatus 枚举与 FetchState 对象类型只存在于客户端;
  2. 客户端类型之间可以互相引用:FetchState.status 字段的类型是同样为客户端类型的 FetchStatus;
  3. 客户端类型可以引用服务端类型:FetchState.started_by 字段指向服务端已有的 User! 类型;
  4. 服务端类型可以引用客户端类型:通过 extend type Item 为服务端类型 Item 增加 fetch_state: FetchState 字段,把客户端类型"挂"到服务端数据模型上。

一旦定义完成,这些数据同样可以经 Relay API 正常读取与更新。

从仓库配置看,schema 扩展文件在编译器配置中是显式可配置的:relay-compiler 的配置结构中包含 schema_extensions: Vec<PathBuf> 字段(见 config.rs),用于收集扩展文件或目录;file_categorizer.rs 中也会依据该配置对扩展文件进行分类处理。因此在实际项目中,你既可以把扩展类型写在 --src 目录下的任意 .graphql 文件中,也可以将其显式声明为独立的 schema extension 路径。


读取客户端数据(Reading Client-Only Data)

读取客户端数据与读取服务端数据没有任何区别:你只需要在 Fragment 或 Query 中像选择普通字段一样选择这些客户端字段即可:

const data = useFragment(
  graphql`
    fragment CommentComponent_comment on Comment {

      # We can select client-only fields as we would any other field
      is_new_comment

      body {
        text
      }
    }
  `,
  props.user,
);

在这个例子中,is_new_comment 是客户端扩展字段,body.text 是服务端字段,二者可以共存于同一个 Fragment 中并被同时读取。

有一点需要留意:当通过 Query(如 useLazyLoadQuery)读取客户端数据时,通常要求查询中至少包含一个服务端字段(可以是普通服务端字段,也可以是 __typename 这类 schema 无关的内省字段),这是因为查询请求仍需以某种方式关联到服务端。更完整的读写本地数据的 Query 示例,可以参见 Client Schema Extensions 指南 中的 "Querying local state" 一节。


更新客户端数据(Updating Client-Only Data)

更新客户端数据有两种常见途径,二者都基于 Relay 对客户端扩展字段的完整支持:

方式一:在 mutation / subscription 的 updater 中更新

你可以在 Mutation 或 Subscription 的 updater 函数中,像操作普通服务端数据一样对客户端字段进行读写。例如创建评论的 mutation 完成后,在 updater 中为 is_new_comment 赋值。

方式二:使用本地更新原语(commitLocalUpdate)

更多时候,纯本地状态的写入并不与任何服务端操作绑定,此时最适合使用 Local Data Updates 中介绍的 commitLocalUpdate。它接收 environment 与一个 updater 函数,updater 拿到 RecordSourceSelectorProxy(即 store 代理),可以命令式地创建新记录、修改或删除已有记录:

import {commitLocalUpdate} from 'react-relay';

commitLocalUpdate(environment, store => {
  // Imperatively mutate the store here
  // e.g. store.create(...), store.get(...).setValue(...), store.delete(...)
});

关于 store 代理的完整接口能力,可参见 Relay Store 的 API 参考。更完整的 create / update / delete 本地数据实例,可参考 Client Schema Extensions 指南 中的 "Mutating local state" 一节。从实现层面看,commitLocalUpdate 的签名(environment: IEnvironment, updater: StoreUpdater)定义于 commitLocalUpdate.js。

使用 commitLocalUpdate 时需要注意:

  • updater 与 mutation/subscription 的 updater 不同,不接收第二个参数,因为没有关联的网络响应 payload;
  • 任何本地数据更新都会自动通知订阅了这些数据的组件并触发重新渲染;
  • 所有本地数据都存储在 Relay Store 中。

数据保留(Retention)注意事项

由 Query / Fragment 渲染的本地数据会被自动保留(retain),不会被垃圾回收。如果没有任何组件渲染某条本地数据、而你希望手动保留它,可以调用 environment.retain(operation) 并持有返回的 disposable,在不需要时调用 disposable.dispose() 释放。具体做法(创建 operation descriptor、调用 retain)详见 Client Schema Extensions 指南 中的 "Mutating local state" 一节。

初始状态(Initial State)

客户端扩展字段的默认值一律为 undefined。如果你希望在查询本地数据之前就设置好初始值,可以在 commitLocalUpdate 的 updater 中"预置"初始状态,例如将某个列表初始化为空数组:

import {commitLocalUpdate} from 'react-relay';

commitLocalUpdate(environment, store => {
  const user = store.getRoot().getLinkedRecord('viewer');
  // initialize user notes to an empty array
  user.setLinkedRecords([], 'notes');
});

编译器的底层处理:转换、校验与代码生成

客户端 Schema 扩展之所以能"像普通字段一样"工作,是因为 Relay Compiler(Rust 实现)在构建流程中对其做了专门处理:

  • Schema 合并解析:在 build_schema.rs 中,服务端 Schema 与扩展 SDL 文档通过 parse_schema_with_extensions_parallel 被并行解析并合并,扩展类型在 Schema 中通过 is_extension 等标记被识别。
  • 客户端选择集转换:client_extensions.rs 实现了一个 transform:把所有客户端选择(client selections)分组并生成 @__clientExtension 内联 Fragment,其生成结果仅用于 codegen 产生 ClientExtension 节点。该 transform 还会缓存"抽象扩展类型是否含有服务端具体实现"的判断结果(见 client_extensions.rs)。
  • 专项校验:编译器包含针对客户端扩展字段的验证器。例如 validate_client_schema_extensions_use_catch.rs 会在 @throwOnFieldError 上下文中校验:由于客户端扩展字段无法保证在读取前一定被赋值,因此必须被 @catch 指令包裹,以优雅处理缺失数据;该校验器在 validate.rs 中被接入项目构建流程。
  • 测试验证:relay-compiler 的集成测试覆盖了多种客户端扩展场景,例如 client_schema_extension_in_throw_on_field_error、client_schema_extension_interface_uses_resolver_type、resolver_returns_client_schema_extension_enum 等(见 relay_compiler_integration_test.rs);relay-lsp 也提供了客户端扩展标量字段的 hover 测试(见 hover_test.rs)。这些测试证明扩展字段在 schema 解析、类型检查、代码生成与编辑器体验中均已得到支持。

这些源码事实表明:client schema extensions 不是"运行时魔法",而是由编译器在构建期完整识别、校验并转换的一等公民能力。


小结

客户端 Schema 扩展(Client-Only Data)让 Relay 从"服务端数据的获取框架"进化为"客户端全部数据的唯一数据源":

能力语法 / API说明
扩展已有类型extend type X { ... }为服务端类型增加客户端字段
定义新类型普通 GraphQL type / enum可互相引用、可引用服务端类型、可被服务端类型引用
读取Fragment / Query 中普通字段选择与服务端字段无差别
更新mutation/subscription updater 或 commitLocalUpdate纯本地更新首选 commitLocalUpdate
初始状态updater 预置新字段默认 undefined
数据保留environment.retain() / 组件自动 retain无组件渲染时需手动 retain

遵循本指南,你便可以在 React 应用中用统一、类型安全的 Relay 数据层同时管理服务端数据与纯客户端状态,从"评论是否为新"这类小标记,到完整的本地业务模型,都可以用同一套读取与更新心智模型来驾驭。

  • 前端
  • 开发工具

【免费下载链接】relay

Relay is a JavaScript framework for building data-driven React applications.

项目地址: https://gitcode.com/gh_mirrors/relay29/relay
点击查看 免费下载
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐