运行时架构
Relay 运行时是一个功能齐全的 GraphQL 客户端,它设计用于即使在低端移动设备上也能实现高性能,并且能够扩展到大型、复杂的应用程序。运行时 API 不打算在产品代码中直接使用,而是为了构建更高级的产品 API(例如 React/Relay)提供基础。该基础包括
- 一个规范化的、内存中的对象图/缓存。
- 一个优化的“写入”操作,用于使用查询/突变/订阅的结果更新缓存。
- 一种机制,用于从缓存中读取数据并订阅更新,当这些结果因突变、订阅更新等而发生变化时。
- 垃圾回收机制,用于在缓存中的条目不再被任何视图引用时将其清除。
- 一个通用的机制,用于在将数据发布到缓存之前拦截数据,并合成新数据或将新数据和现有数据合并在一起(这在其他功能中允许创建各种分页方案)。
- 具有乐观更新的突变,以及使用任意逻辑更新缓存的能力。
- 支持网络/服务器支持的实时查询。
- 用于启用订阅的核心原语。
- 用于构建离线/持久化缓存的核心原语。
数据类型
DataID
(类型): 记录的全局唯一或客户端生成的标识符,存储为字符串。Record
(类型): 表示具有标识、类型和字段的独特数据实体。请注意,实际的运行时表示对于系统来说是不透明的:对Record
对象的所有访问(包括记录创建)都是通过RelayModernRecord
模块进行的中介。这允许在单个位置更改表示本身(例如,使用Map
或自定义类)。重要的是,其他代码不要假设Record
始终是普通对象。RecordSource
(类型): 按其数据 ID 键控的记录集合,用于表示缓存及其更新。例如,存储的记录缓存是RecordSource
,查询/突变/订阅的结果被规范化为RecordSource
,这些RecordSource
被发布到存储中。源还定义了用于异步加载记录的方法,以便(最终)支持离线用例。目前,该接口的唯一实现是RelayInMemoryRecordSource
;未来的实现可能会添加对从磁盘加载记录的支持。Store
(类型):RelayRuntime
实例的真相来源,以RecordSource
的形式保存规范的记录集(尽管这不是必需的)。目前唯一实现的是RelayModernStore
。Network
(类型): 提供从外部数据源获取查询数据和执行突变的方法。Environment
(类型): 表示将Store
和Network
结合在一起的封装环境,提供与两者交互的高级 API。这是RelayRuntime
的主要公共 API。
用于处理查询及其结果的类型包括
Selector
(类型): 选择器定义了遍历图的起点,用于定位子图,将 GraphQL 片段、变量和用于遍历应开始的根对象的 Data ID 结合在一起。直观地说,这“选择”了对象图的一部分。Snapshot
(类型): 在给定时间点执行Selector
的(不可变)结果。这包括选择器本身、执行它的结果以及从其检索数据的 Data ID 列表(在确定这些结果何时可能发生变化时很有用)。
数据模型
Relay 运行时设计用于与 GraphQL 架构一起使用,GraphQL 架构描述对象图,其中对象具有类型、标识和一组具有值的字段。对象可以相互引用,这由字段的值是图中的一个或多个其他对象来表示。[1]. 为了与 JavaScript Object
区分,这些数据单元被称为 Record
。Relay 将其内部缓存以及查询/突变/等结果表示为数据 ID 到记录的映射。数据 ID 是记录的唯一(相对于缓存)标识符 - 它可能是实际 id
字段的值,也可能是基于从最近具有 id
的对象的路径到记录(这种基于路径的 ID 被称为客户端 ID)。每个 Record
存储其数据 ID、类型和任何已获取的字段。多个记录一起存储为 RecordSource
:数据 ID 到 Record
实例的映射。
例如,用户及其地址可能表示如下
// GraphQL Fragment
fragment on User {
id
name
address {
city
}
}
// Response
{
id: '842472',
name: 'Joe',
address: {
city: 'Seattle',
}
}
// Normalized Representation
RecordSource {
'842472': Record {
__id: '842472',
__typename: 'User', // the type is known statically from the fragment
id: '842472',
name: 'Joe',
address: {__ref: 'client:842472:address'}, // link to another record
},
'client:842472:address': Record {
// A client ID, derived from the path from parent & parent's ID
__id: 'client:842472:address',
__typename: 'Address',
city: 'Seattle',
}
}
[1]请注意,GraphQL 本身并没有强加这种约束,Relay 运行时也可以用于不符合这种约束的架构。例如,两个系统都可以用于查询单个非规范化表。但是,Relay 运行时提供的许多功能,例如缓存和规范化,在将数据表示为具有离散信息稳定标识的规范化图时效果最佳。
存储操作
Store
是应用程序数据的真相来源,并提供以下核心操作。
lookup(selector: Selector): Snapshot
: 从存储中读取选择器的结果,返回给定存储中当前数据的值。subscribe(snapshot: Snapshot, callback: (snapshot: Snapshot) => void): Disposable
: 订阅对选择器结果的更改。当数据发布到存储中会导致快照的选择器结果发生变化时,就会调用回调。publish(source: RecordSource): void
: 使用新信息更新存储。所有对存储的更新都以这种形式表达,包括查询/突变/订阅的结果以及乐观突变更新。所有这些操作在内部都会创建一个新的RecordSource
实例,并最终将其发布到存储中。请注意,publish()
不会立即更新任何subscribe()
-ers。在内部,存储会将新的RecordSource
与其内部源进行比较,并在需要时进行更新。- 仅存在于已发布源中的记录将添加到存储中。
- 同时存在于两者的记录将合并到一个新记录中(输入不变),并将结果添加到存储中。
- 在已发布源中为 null 的记录将在存储中删除(设置为 null)。
- 具有特殊哨兵值的记录将从存储中删除。这支持取消发布乐观创建的记录。
notify(): void
: 调用任何subscribe()
-ers,其结果因介入的publish()
-es 而发生了变化。将publish()
和notify()
分开,可以在执行任何下游更新逻辑(例如渲染)之前发布多个有效负载。retain(selector: Selector): Disposable
: 确保实现给定选择器所需的所有记录都保留在内存中。在返回的引用被释放之前,这些记录将不会有资格进行垃圾回收。
示例数据流:获取查询数据
┌───────────────────────┐
│ Query │
└───────────────────────┘
│
▼
┌ ─ ─ ─ ┐
fetch ◀────────────▶ Server
└ ─ ─ ─ ┘
│
┌─────┴───────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ Query │ │ Response │
└──────────┘ └──────────┘
│ │
└─────┬───────┘
│
▼
normalize
│
▼
┌───────────────────────┐
│ RecordSource │
│ │
│┌──────┐┌──────┐┌─────┐│
││Record││Record││ ... ││
│└──────┘└──────┘└─────┘│
└───────────────────────┘
- 查询将从网络获取。
- 查询和响应一起遍历,将结果提取到
Record
对象中,这些对象被添加到一个新的RecordSource
中。
这个新的 RecordSource
然后将被发布到存储中。
publish
│
▼
┌───────────────────────────┐
│ Store │
│ ┌───────────────────────┐ │
│ │ RecordSource │ │
│ │ │ │
│ │┌──────┐┌──────┐┌─────┐│ │
│ ││Record││Record││ ... ││ │ <--- records are updated
│ │└──────┘└──────┘└─────┘│ │
│ └───────────────────────┘ │
│ ┌───────────────────────┐ │
│ │ Subscriptions │ │
│ │ │ │
│ │┌──────┐┌──────┐┌─────┐│ │
│ ││ Sub. ││ Sub. ││ ... ││ │ <--- subscriptions do not fire yet
│ │└──────┘└──────┘└─────┘│ │
│ └───────────────────────┘ │
└───────────────────────────┘
发布结果会更新存储,但不会立即通知任何订阅者。这是通过调用 notify()
完成的……
notify
│
▼
┌───────────────────────────┐
│ Store │
│ ┌───────────────────────┐ │
│ │ RecordSource │ │
│ │ │ │
│ │┌──────┐┌──────┐┌─────┐│ │
│ ││Record││Record││ ... ││ │
│ │└──────┘└──────┘└─────┘│ │
│ └───────────────────────┘ │
│ ┌───────────────────────┐ │
│ │ Subscriptions │ │
│ │ │ │
│ │┌──────┐┌──────┐┌─────┐│ │
│ ││ Sub.││ Sub.││ ...││ │ <--- affected subscriptions fire
│ │└──────┘└──────┘└─────┘│ │
│ └───┼───────┼───────┼───┘ │
└─────┼───────┼───────┼─────┘
│ │ │
▼ │ │
callback │ │
▼ │
callback │
▼
callback
…这会调用任何其结果发生变化的 subscribe()
-ers 的回调。每个订阅都会按如下方式进行检查
- 首先,将自上次
notify()
以来发生更改的数据 ID 列表与订阅的最新Snapshot
中列出的数据 ID 进行比较。如果没有重叠,则订阅的结果不可能发生变化(如果你想象图的视觉效果,则发生变化的部分图与被选择的图部分之间没有重叠)。在这种情况下,订阅将被忽略,否则继续处理。 - 其次,任何具有重叠数据 ID 的订阅都会被重新读取,并将新/旧结果进行比较。如果结果没有发生变化,则订阅将被忽略(如果记录的某个字段发生了变化,但与订阅的选择器无关,则可能发生这种情况),否则继续处理。
- 最后,数据实际发生变化的订阅将通过其回调进行通知。
示例数据流:读取和观察存储
产品主要通过 lookup()
和 subscribe()
访问存储。Lookup 读取片段的初始结果,subscribe 观察该结果以进行任何更改。请注意,lookup()
的输出 - 一个 Snapshot
- 是 subscribe()
的输入。这很重要,因为快照包含可用于优化订阅的重要信息 - 如果 subscribe()
仅接受 Selector
,它将不得不重新读取结果以了解要订阅的内容,这效率较低。
因此,典型的数据流如下 - 请注意,此流程由更高级别的 API(如 React/Relay)自动管理。首先,组件将针对记录源(例如存储的规范源)查找选择器的结果。
┌───────────────────────┐ ┌──────────────┐
│ RecordSource │ │ │
│ │ │ │
│┌──────┐┌──────┐┌─────┐│ │ Selector │
││Record││Record││ ... ││ │ │
│└──────┘└──────┘└─────┘│ │ │
└───────────────────────┘ └──────────────┘
│ │
│ │
└──────────────┬────────────┘
│
│ lookup
│ (read)
│
▼
┌─────────────┐
│ │
│ Snapshot │
│ │
└─────────────┘
│
│ render, etc
│
▼
接下来,它将使用此快照 subscribe()
,以便在发生任何更改时收到通知 - 请参阅上图以了解 publish()
和 notify()
。
此页面是否有用?
通过以下方式帮助我们改进网站 回答几个快速问题.