執行階段架構
Relay 執行階段是一個功能完整的 GraphQL 客戶端,專為即使在低階行動裝置上也能保持高效能而設計,並且能夠擴展到大型、複雜的應用程式。執行階段 API 並非旨在直接在產品程式碼中使用,而是為建構更高等級的產品 API(例如 React/Relay)提供基礎。此基礎包含:
- 一個標準化的記憶體內物件圖/快取。
- 一個最佳化的「寫入」操作,用於以查詢/變異/訂閱的結果更新快取。
- 一種從快取讀取資料,並在變異、訂閱更新等導致這些結果變更時訂閱更新的機制。
- 垃圾回收機制,用於在快取中的條目不再被任何視圖引用時將其逐出。
- 一種通用機制,用於在將資料發佈到快取之前攔截資料,並合成新資料或將新資料與現有資料合併(除其他外,可建立各種分頁方案)。
- 具有樂觀更新的變異,以及使用任意邏輯更新快取的能力。
- 支援網路/伺服器支援的即時查詢。
- 啟用訂閱的核心基本要素。
- 用於建構離線/持久快取的核心基本要素。
資料類型
DataID
(類型):記錄的全局唯一或客戶端生成的識別符,儲存為字串。Record
(類型):具有身分、類型和欄位的個別資料實體的表示形式。請注意,實際的執行階段表示形式對系統是不透明的:所有對Record
物件的存取(包括記錄建立)都透過RelayModernRecord
模組進行調解。這允許在單一位置更改表示形式本身(例如,使用Map
或自訂類別)。重要的是,其他程式碼不應假設Record
將始終是純粹的物件。RecordSource
(類型):以其資料 ID 為鍵的記錄集合,用於表示快取及其更新。例如,儲存的記錄快取是一個RecordSource
,查詢/變異/訂閱的結果會標準化為RecordSource
並發佈到儲存中。來源還定義了用於非同步載入記錄的方法,以便(最終)支援離線使用案例。目前,此介面的唯一實作是RelayInMemoryRecordSource
;未來的實作可能會增加從磁碟載入記錄的支援。Store
(類型):RelayRuntime
實例的真實來源,以RecordSource
的形式保存標準的記錄集(儘管這不是必需的)。目前唯一的實作是RelayModernStore
。Network
(類型):提供從外部資料來源擷取查詢資料和執行變異的方法。Environment
(類型):表示結合了Store
和Network
的封裝環境,提供與兩者互動的高階 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││ ... ││
│└──────┘└──────┘└─────┘│
└───────────────────────┘
- 從網路擷取查詢。
- 查詢和回應會一起遍歷,將結果擷取到
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()
訂閱者呼叫回呼。每個訂閱都會按照以下方式檢查:
- 首先,會將自上次
notify()
以來已變更的資料 ID 列表與訂閱最新Snapshot
中列出的資料 ID 進行比較。如果沒有重疊,訂閱的結果就不可能變更(如果您在視覺上想像圖形,則變更的圖形部分與選擇的部分之間沒有重疊)。在這種情況下,會忽略訂閱,否則會繼續處理。 - 其次,會重新讀取任何具有重疊資料 ID 的訂閱,並比較新的/先前的結果。如果結果沒有變更,則會忽略訂閱(如果記錄的欄位變更,但與訂閱的選擇器無關,則可能會發生這種情況),否則會繼續處理。
- 最後,會透過回呼通知資料實際變更的訂閱。
範例資料流:讀取和觀察儲存
產品主要透過 lookup()
和 subscribe()
存取儲存。查找會讀取片段的初始結果,訂閱會觀察該結果的任何變更。請注意,lookup()
的輸出(即 Snapshot
)是 subscribe()
的輸入。這一點很重要,因為快照包含可用於最佳化訂閱的重要資訊 - 如果 subscribe()
僅接受 Selector
,則必須重新讀取結果,以便知道要訂閱什麼,這樣效率會較低。
因此,典型的資料流程如下 - 請注意,此流程由較高階的 API(例如 React/Relay)自動管理。首先,元件會針對記錄來源(例如,儲存的標準來源)查找選擇器的結果
┌───────────────────────┐ ┌──────────────┐
│ RecordSource │ │ │
│ │ │ │
│┌──────┐┌──────┐┌─────┐│ │ Selector │
││Record││Record││ ... ││ │ │
│└──────┘└──────┘└─────┘│ │ │
└───────────────────────┘ └──────────────┘
│ │
│ │
└──────────────┬────────────┘
│
│ lookup
│ (read)
│
▼
┌─────────────┐
│ │
│ Snapshot │
│ │
└─────────────┘
│
│ render, etc
│
▼
接下來,它將使用此快照進行 subscribe()
,以便在任何變更時收到通知 - 請參閱上述 publish()
和 notify()
的圖表。
此頁面是否有用?
協助我們透過以下方式讓網站更加完善: 回答幾個簡單的問題.