互動查詢
我們已經了解片段如何讓我們在每個元件中指定資料需求,但在執行階段僅為整個畫面執行單一查詢。在這裡,我們將探討在同一個畫面上想要執行第二個查詢的情況。這也將讓我們探索 GraphQL 查詢的更多功能。
- 我們將建立一個懸浮卡片,當您將滑鼠懸停在故事發布者的名稱上時,會顯示有關發布者的更多詳細資訊。
- 懸浮卡片將使用第二個查詢來提取額外資訊,這些資訊僅在使用者懸停時才需要。
- 我們將使用查詢變數來告訴伺服器我們想要更多關於哪個人的詳細資訊。
- 我們將了解如何透過預先載入查詢來提高效能。
在涵蓋這些主題之後,我們將返回來查看片段的一些更進階功能。
在本節中,我們將在 PosterByline
中新增一個懸浮卡片,讓您可以透過將滑鼠懸停在他們的名稱上來查看更多關於故事發布者的詳細資訊。
深入探討:何時使用輔助查詢
我們之前曾提到,Relay 的設計目的是協助您預先擷取整個畫面的所有資料需求。但我們可以將其概括為:一個使用者互動最多只能有一個查詢。導覽至另一個畫面只是一種常見的使用者互動類型。
在一個畫面中,某些互動可能會顯示最初顯示的額外資料。如果互動的執行次數相對較少,但需要大量的額外資料,則在互動發生時執行第二個查詢來擷取額外資料是明智之舉,而不是在畫面首次載入時預先擷取。這可以使初始載入速度更快且成本更低。
還有一些互動中,擷取的資料量是不確定的 — 例如,懸浮卡片中的懸浮卡片 — 且無法靜態得知。
如果資料的優先順序較低,並且應在主要資料載入後載入,但應在沒有進一步使用者輸入的情況下自動彈出,則 Relay 有一個稱為延遲片段的功能。我們稍後會介紹。
我們已經準備好一個懸浮卡片元件供您使用。但是,它一直位於名為 future
的目錄中,以避免編譯錯誤,因為它使用 ImageFragment
。現在我們已經到了本教學的此階段,您可以將 future
中的模組移至 src/components
mv future/* src/components
現在,如果您已完成練習以使 PosterByline
使用片段,則 PosterByline
元件應如下所示
export default function PosterByline({ poster }: Props): React.ReactElement {
const data = useFragment(PosterBylineFragment, poster);
return (
<div className="byline">
<Image image={data.profilePicture} width={60} height={60} className="byline__image" />
<div className="byline__name">{data.name}</div>
</div>
);
}
若要使用懸浮卡片元件,您可以進行以下變更
import Hovercard from './Hovercard';
import PosterDetailsHovercardContents from './PosterDetailsHovercardContents';
const {useRef} = React;
...
export default function PosterByline({ poster }: Props): React.ReactElement {
const data = useFragment(PosterBylineFragment, poster);
const hoverRef = useRef(null);
return (
<div
ref={hoverRef}
className="byline">
<Image image={data.profilePicture} width={60} height={60} className="byline__image" />
<div className="byline__name">{data.name}</div>
<Hovercard targetRef={hoverRef}>
<PosterDetailsHovercardContents />
</Hovercard>
</div>
);
}
您現在應該看到,每當您將滑鼠懸停在某人的姓名上時,都會出現一個包含更多資訊的懸浮卡片。如果您查看 PosterDetailsHovercardContents.tsx
內部,您會發現它會使用 useLazyLoadQuery
執行第二個查詢,以在該元件掛載時擷取額外資訊。
只有一個問題:無論您將滑鼠懸停在哪個發布者上,它總是顯示同一個人的資訊!
查詢變數
我們需要告訴伺服器我們想要哪個資訊的更多資訊。GraphQL 可讓我們定義查詢變數,這些變數可以作為引數傳遞給特定欄位。然後,這些引數在伺服器上可用。
在上一節中,我們看到欄位如何接受引數,但引數值是硬式編碼的,例如 url(width: 200, height: 200)
。透過查詢變數,我們可以在執行階段決定這些值。它們與查詢本身一起從用戶端傳遞到伺服器。GraphQL 變數一律以 $
美元符號開頭。
查看 PosterDetailsHovercardContents.tsx
內部:您應該看到類似這樣的查詢
const PosterDetailsHovercardContentsQuery = graphql`
query PosterDetailsHovercardContentsQuery {
node(id: "1") {
... on Actor {
...PosterDetailsHovercardContentsBodyFragment
}
}
}
`;
node
欄位是在我們的結構描述中定義的頂層欄位,可讓您擷取任何給定唯一 ID 的圖形節點。它以 ID 作為引數,目前是硬式編碼的。在本練習中,我們將用 UI 狀態提供的變數取代此硬式編碼的 ID。看起來很有趣的 ... on Actor
是一個 類型精化。我們將在下一節中更詳細地探討這些,並且現在可以忽略它。簡而言之,由於我們可以向 node
欄位提供任何 ID,因此無法靜態地知道我們將選擇什麼類型的節點。類型精化指定了我們預期的類型,讓我們可以使用 Actor
類型的欄位
在其中,我們只會展開一個包含我們要顯示的欄位的片段 — 稍後會詳細介紹。現在,以下是將此硬式編碼的 ID 替換為我們懸停的發布者 ID 所需的步驟
步驟 1 — 定義查詢變數
首先,我們需要編輯查詢,以宣告它接受查詢變數。以下是變更
const PosterDetailsHovercardContentsQuery = graphql`
query PosterDetailsHovercardContentsQuery(
$posterID: ID!
) {
node(id: "1") {
... on Actor {
...PosterDetailsHovercardContentsBodyFragment
}
}
}
`;
- 變數名稱為
$posterID
。這是我們將在 GraphQL 查詢的其餘部分中使用的符號,以引用從 UI 傳入的值。 - 變數有一個類型 — 在此案例中為
ID!
。ID
類型是String
的同義詞,用於節點 ID,以協助將它們與其他字串區分開來。ID!
上的!
表示該欄位不可為 null。在 GraphQL 中,欄位通常可為 null,而不可為 null 是一種例外。
步驟 2 — 將變數作為欄位引數傳入
現在,我們將硬式編碼的 "1"
替換為我們的新變數
const PosterDetailsHovercardContentsQuery = graphql`
query PosterDetailsHovercardContentsQuery($posterID: ID!) {
node(
id: $posterID
) {
... on Actor {
...PosterDetailsHovercardContentsBodyFragment
}
}
}
`;
您不僅可以使用查詢變數作為欄位引數,還可以作為片段的引數。
步驟 3 — 將引數值提供給 useLazyLoadQuery
現在我們需要在執行階段從我們的 UI 傳入實際值。useLazyLoadQuery
勾點的第二個引數是一個包含變數值的物件。我們將為元件新增一個新的 prop,並將其值傳遞到其中
export default function PosterDetailsHovercardContents({
posterID,
}: {
posterID: string;
}): React.ReactElement {
const data = useLazyLoadQuery<QueryType>(
PosterDetailsHovercardContentsQuery,
{posterID},
);
return <PosterDetailsHovercardContentsBody poster={data.node} />;
}
步驟 4 — 從父元件傳入 ID
現在,我們需要從懸浮卡片的父元件(即 PosterByline
)提供 posterID
prop。前往該檔案並將 id
新增至其片段 — 然後將 ID 作為 prop 傳入
const PosterBylineFragment = graphql`
fragment PosterBylineFragment on Actor {
id
...
}
`;
export default function PosterByline({ poster }: Props): React.ReactElement {
...
return (
...
<PosterDetailsHovercardContents
posterID={data.id}
/>
...
);
}
此時,懸浮卡片應該會顯示我們懸停的每個發布者的適當資訊。
如果您在瀏覽器中使用網路檢測器,您應該能夠找到變數值正與查詢一起傳遞
您可能還會注意到,此請求僅在您第一次將滑鼠懸停在特定發布者上時提出。Relay 會快取查詢的結果並在之後重複使用它們,直到最終移除最近未使用的快取資料。
深入探討:快取和 Relay 儲存
與大多數其他系統不同,Relay 的快取不是基於查詢,而是基於圖形節點。Relay 維護其擷取的所有節點的本地快取,稱為 Relay 儲存。儲存中的每個節點都會根據其 ID 來識別和擷取。如果兩個查詢要求相同的資訊(如節點 ID 所識別),則第二個查詢將使用為第一個查詢擷取的快取資訊來滿足,而不會擷取。請務必設定遺失欄位處理程式,以利用此快取行為。
如果 Relay 中 Store 的節點在任何已使用或最近被掛載的元件所使用的查詢中「無法存取」,Relay 就會將其垃圾回收。
深入探討:為什麼 GraphQL 需要變數語法
您可能想知道為什麼 GraphQL 會有變數的概念,而不是直接將變數的值插入查詢字串中。嗯,如前所述,GraphQL 查詢字串的文本在運行時不可用,因為 Relay 會將其替換為更有效率的資料結構。您還可以設定 Relay 使用預備查詢,讓編譯器在建置時將每個查詢上傳到伺服器並分配一個 ID — 在這種情況下,在運行時,Relay 只是告訴伺服器「給我查詢 #1337」,因此字串插值是不可行的,因此變數必須以帶外方式傳遞。即使查詢字串可用,單獨傳遞變數值也能消除序列化任意值和跳脫字串的任何問題,而這些問題超出了任何 HTTP 請求的要求。
預先載入的查詢
這個範例應用程式非常簡單,因此效能不是問題。(事實上,伺服器被人為地降低速度,以便讓載入狀態可以被感知到。)然而,Relay 的主要考量之一是在真實應用程式中盡可能提高效能。
現在,hovercard 使用 useLazyLoadQuery
hook,這會在元件渲染時提取查詢。這表示時間軸看起來像這樣
理想情況下,我們應該盡可能提早開始網路提取,但在這裡,我們直到 React 完成渲染才開始。如果我們使用 React.lazy
來載入 hovercard 元件本身的程式碼,那麼這個時間軸可能會更糟。在這種情況下,它看起來會像這樣
請注意,我們在開始提取 GraphQL 查詢之前一直在等待。如果查詢提取在 React 元件渲染之前就開始,直接在滑鼠事件處理器本身開始,那就更好了。那麼時間軸看起來會像這樣
當使用者互動時,我們應該立即開始提取我們需要的查詢,同時開始渲染元件(如果需要,先提取其程式碼)。一旦這兩個非同步過程都完成,我們就可以渲染具有可用資料的元件並將其顯示給使用者。
Relay 提供了一項稱為預先載入的查詢的功能,可讓我們做到這一點。
讓我們修改 hovercard 以使用預先載入的查詢。
步驟 1 — 將 useLazyLoadQuery 變更為 usePreloadedQuery
提醒一下,這是目前以延遲方式提取資料的 PosterDetailsHovercardContents
元件
export default function PosterDetailsHovercardContents({
posterID,
}: {
posterID: string;
}): React.ReactElement {
const data = useLazyLoadQuery<QueryType>(
PosterDetailsHovercardContentsQuery,
{posterID},
);
return <PosterDetailsHovercardContentsBody poster={data.node} />;
}
它會呼叫 useLazyLoadQuery
,該 hook 接受變數作為其第二個引數。我們想要將其變更為 usePreloadedQuery
。然而,對於預先載入的查詢,變數實際上是在提取查詢時決定的,這將在元件渲染之前發生。因此,這個 hook 並非使用變數,而是接收一個查詢參考,其中包含它擷取查詢結果所需的信息。查詢參考將在我們於步驟 2 中提取查詢時建立。
依以下方式變更元件
import {usePreloadedQuery} from 'react-relay';
import type {PreloadedQuery} from 'react-relay';
import type {PosterDetailsHovercardContentsQuery as QueryType} from './__generated__/PosterDetailsHovercardContentsQuery.graphql';
export default function PosterDetailsHovercardContents({
queryRef,
}: {
queryRef: PreloadedQuery<QueryType>,
}): React.ReactElement {
const data = usePreloadedQuery(
PosterDetailsHovercardContentsQuery,
queryRef,
);
...
}
步驟 2:匯出查詢,以便從父元件存取
我們將修改父元件 PosterByline
,使其初始化 PosterDetailsHovercardContentsQuery
查詢。它需要該查詢的參考,因此我們需要匯出它
export const PosterDetailsHovercardContentsQuery = graphql`...
步驟 3:在父元件中呼叫 useQueryLoader
現在 PosterDetailsHovercardContents
預期會接收查詢參考,我們需要建立該查詢參考並從父元件(也就是 PosterByline
)傳遞下來。我們使用一個稱為 useQueryLoader
的 hook 來建立查詢參考。這個 hook 也會傳回一個函數,我們會在我們的事件處理器中呼叫該函數以觸發查詢提取。
import {useQueryLoader} from 'react-relay';
import type {PosterDetailsHovercardContentsQuery as HovercardQueryType} from './__generated__/PosterDetailsHovercardContentsQuery.graphql';
import {PosterDetailsHovercardContentsQuery} from './PosterDetailsHovercardContents';
export default function PosterByline({ poster }: Props): React.ReactElement {
...
const [
hovercardQueryRef,
loadHovercardQuery,
] = useQueryLoader<HovercardQueryType>(PosterDetailsHovercardContentsQuery);
return (
...
<PosterDetailsHovercardContents
queryRef={hovercardQueryRef}
/>
...
);
}
useQueryLoader
hook 會傳回我們需要的兩件事
- 查詢參考是一段不透明的資訊,
usePreloadedQuery
會使用它來擷取查詢的結果。 loadHovercardQuery
是一個將會啟動請求的函數。
步驟 4:在事件處理器中提取查詢
最後,我們需要在卡片顯示時發生的事件處理器中呼叫 loadHovercardQuery
。幸運的是,Hovercard
元件有一個 onBeginHover
事件可供我們使用
export default function PosterByline({ poster }: Props): React.ReactElement {
...
const [
hovercardQueryRef,
loadHovercardQuery,
] = useQueryLoader<HovercardQueryType>(PosterDetailsHovercardContentsQuery);
function onBeginHover() {
loadHovercardQuery({posterID: data.id});
}
return (
<div className="byline">
...
<Hovercard
onBeginHover={onBeginHover}
targetRef={hoverRef}>
<PosterDetailsHovercardContents queryRef={hovercardQueryRef} />
</Hovercard>
</div>
);
}
請注意,查詢變數現在會在這裡傳遞,而我們在此處啟動請求。
此時,您應該會看到與之前相同的行為,但現在它會稍微快一點,因為 Relay 可以更早開始查詢。
雖然我們為了簡單起見而使用 useLazyLoadQuery
來介紹查詢,但預先載入的查詢始終是 Relay 中使用查詢的首選方式,因為它們可以顯著提高現實世界中的效能。透過與您的伺服器和路由系統進行適當的 整合,您甚至可以在下載或執行任何用戶端程式碼之前,在伺服器端預先載入網頁的主要查詢。
總結
- 雖然畫面上最初顯示的所有資料都應該合併到一個查詢中,但需要更多資訊的使用者互動可以使用次要查詢來處理。
- 查詢變數可讓您將資訊與查詢一起傳遞到伺服器。
- 查詢變數是藉由將它們傳遞到欄位引數中來使用。
- 預先載入的查詢永遠是最佳選擇。對於使用者互動查詢,請在事件處理器中啟動提取。對於畫面的初始查詢,請盡早在特定路由系統中啟動提取。請僅將延遲載入的查詢用於快速原型設計,或完全不要使用它們。
接下來,我們將簡要地了解如何透過處理不同類型的海報來增強 hovercard。之後,我們將了解如何處理初始查詢中包含的資訊也需要以不同變數更新和重新提取的情況。