跳至主要內容
版本:v18.0.0

執行階段架構

Relay 執行階段是一個功能完整的 GraphQL 客戶端,專為即使在低階行動裝置上也能保持高效能而設計,並且能夠擴展到大型、複雜的應用程式。執行階段 API 並非旨在直接在產品程式碼中使用,而是為建構更高等級的產品 API(例如 React/Relay)提供基礎。此基礎包含:

  • 一個標準化的記憶體內物件圖/快取。
  • 一個最佳化的「寫入」操作,用於以查詢/變異/訂閱的結果更新快取。
  • 一種從快取讀取資料,並在變異、訂閱更新等導致這些結果變更時訂閱更新的機制。
  • 垃圾回收機制,用於在快取中的條目不再被任何視圖引用時將其逐出。
  • 一種通用機制,用於在將資料發佈到快取之前攔截資料,並合成新資料或將新資料與現有資料合併(除其他外,可建立各種分頁方案)。
  • 具有樂觀更新的變異,以及使用任意邏輯更新快取的能力。
  • 支援網路/伺服器支援的即時查詢。
  • 啟用訂閱的核心基本要素。
  • 用於建構離線/持久快取的核心基本要素。

資料類型

  • DataID(類型):記錄的全局唯一或客戶端生成的識別符,儲存為字串。
  • Record(類型):具有身分、類型和欄位的個別資料實體的表示形式。請注意,實際的執行階段表示形式對系統是不透明的:所有對 Record 物件的存取(包括記錄建立)都透過 RelayModernRecord 模組進行調解。這允許在單一位置更改表示形式本身(例如,使用 Map 或自訂類別)。重要的是,其他程式碼不應假設 Record 將始終是純粹的物件。
  • RecordSource(類型):以其資料 ID 為鍵的記錄集合,用於表示快取及其更新。例如,儲存的記錄快取是一個 RecordSource,查詢/變異/訂閱的結果會標準化為 RecordSource 並發佈到儲存中。來源還定義了用於非同步載入記錄的方法,以便(最終)支援離線使用案例。目前,此介面的唯一實作是 RelayInMemoryRecordSource;未來的實作可能會增加從磁碟載入記錄的支援。
  • Store(類型):RelayRuntime 實例的真實來源,以 RecordSource 的形式保存標準的記錄集(儘管這不是必需的)。目前唯一的實作是 RelayModernStore
  • Network(類型):提供從外部資料來源擷取查詢資料和執行變異的方法。
  • Environment(類型):表示結合了 StoreNetwork 的封裝環境,提供與兩者互動的高階 API。這是 RelayRuntime 的主要公開 API。

用於處理查詢及其結果的類型包括:

  • Selector(類型):選擇器定義圖遍歷的起點,以便針對子圖,結合 GraphQL 片段、變數以及從中應開始遍歷的根物件的資料 ID。直觀地說,這「選擇」了物件圖的一部分。
  • Snapshot(類型):在給定時間點執行 Selector 的(不可變)結果。這包括選擇器本身、執行結果以及從中檢索資料的資料 ID 列表(有助於確定這些結果何時可能變更)。

資料模型

Relay 執行階段旨在與 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() 訂閱者。在內部,儲存會將新的 RecordSource 與其內部來源進行比較,並在必要時更新它。

    • 僅存在於已發佈來源中的記錄會新增至儲存。
    • 同時存在於兩者中的記錄會合併到新的記錄中(輸入保持不變),結果會新增至儲存。
    • 已發佈來源中為空的記錄會在儲存中刪除(設定為 null)。
    • 具有特殊哨兵值的記錄會從儲存中移除。這支援取消發佈樂觀建立的記錄。
  • notify(): void:呼叫由於介入的 publish() 而導致結果變更的任何 subscribe() 訂閱者。將 publish()notify() 分開,允許在執行任何下游更新邏輯(例如呈現)之前發佈多個承載。

  • retain(selector: Selector): Disposable:確保所有滿足給定選擇器所需的記錄都保留在記憶體中。在處置傳回的引用之前,記錄將不符合垃圾回收的條件。

範例資料流:擷取查詢資料


┌───────────────────────┐
Query
└───────────────────────┘


┌ ─ ─ ─ ┐
fetch ◀────────────▶ Server
└ ─ ─ ─ ┘

┌─────┴───────┐
▼ ▼
┌──────────┐ ┌──────────┐
Query │ │ Response
└──────────┘ └──────────┘
│ │
└─────┬───────┘


normalize


┌───────────────────────┐
RecordSource
│ │
│┌──────┐┌──────┐┌─────┐│
││Record││Record││ ... ││
│└──────┘└──────┘└─────┘│
└───────────────────────┘

  1. 從網路擷取查詢。
  2. 查詢和回應會一起遍歷,將結果擷取到 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() 訂閱者呼叫回呼。每個訂閱都會按照以下方式檢查:

  1. 首先,會將自上次 notify() 以來已變更的資料 ID 列表與訂閱最新 Snapshot 中列出的資料 ID 進行比較。如果沒有重疊,訂閱的結果就不可能變更(如果您在視覺上想像圖形,則變更的圖形部分與選擇的部分之間沒有重疊)。在這種情況下,會忽略訂閱,否則會繼續處理。
  2. 其次,會重新讀取任何具有重疊資料 ID 的訂閱,並比較新的/先前的結果。如果結果沒有變更,則會忽略訂閱(如果記錄的欄位變更,但與訂閱的選擇器無關,則可能會發生這種情況),否則會繼續處理。
  3. 最後,會透過回呼通知資料實際變更的訂閱。

範例資料流:讀取和觀察儲存

產品主要透過 lookup()subscribe() 存取儲存。查找會讀取片段的初始結果,訂閱會觀察該結果的任何變更。請注意,lookup() 的輸出(即 Snapshot)是 subscribe() 的輸入。這一點很重要,因為快照包含可用於最佳化訂閱的重要資訊 - 如果 subscribe() 僅接受 Selector,則必須重新讀取結果,以便知道要訂閱什麼,這樣效率會較低。

因此,典型的資料流程如下 - 請注意,此流程由較高階的 API(例如 React/Relay)自動管理。首先,元件會針對記錄來源(例如,儲存的標準來源)查找選擇器的結果


┌───────────────────────┐ ┌──────────────┐
RecordSource │ │ │
│ │ │ │
│┌──────┐┌──────┐┌─────┐│ │ Selector
││Record││Record││ ... ││ │ │
│└──────┘└──────┘└─────┘│ │ │
└───────────────────────┘ └──────────────┘
│ │
│ │
└──────────────┬────────────┘

│ lookup
(read)


┌─────────────┐
│ │
Snapshot
│ │
└─────────────┘

│ render, etc



接下來,它將使用此快照進行 subscribe(),以便在任何變更時收到通知 - 請參閱上述 publish()notify() 的圖表。


此頁面是否有用?

協助我們透過以下方式讓網站更加完善: 回答幾個簡單的問題.