片段
片段是 Relay 的顯著特徵之一。它們讓每個元件獨立宣告自己的資料需求,同時保留單一查詢的效率。在本節中,我們將展示如何將查詢拆分成片段。
首先,假設我們希望 Story 元件顯示貼文的日期。為此,我們需要從伺服器取得更多資料,因此我們必須在查詢中新增一個欄位。
前往 Newsfeed.tsx
並找到 NewsfeedQuery
,以便您可以新增新欄位
const NewsfeedQuery = graphql`
query NewsfeedQuery {
topStory {
title
summary
createdAt // Add this line
poster {
name
profilePicture {
url
}
}
image {
url
}
}
}
`;
現在我們已經更新了查詢,我們需要執行 Relay 編譯器,使其了解更新後的 Graphql 查詢,方法是執行 npm run relay
。
接下來,前往 Story.tsx
並修改它以顯示日期
import Timestamp from './Timestamp';
type Props = {
story: {
createdAt: string; // Add this line
...
};
};
export default function Story({story}: Props) {
return (
<Card>
<PosterByline poster={story.poster} />
<Heading>{story.title}</Heading>
<Timestamp time={story.createdAt} /> // Add this line
<Image image={story.image} />
<StorySummary summary={story.summary} />
</Card>
);
}
現在應該會顯示日期。而且由於 GraphQL,我們不必撰寫和部署任何新的伺服器程式碼。
但如果您仔細想想,為什麼您必須修改 Newsfeed.tsx
?React 元件不應該是獨立的嗎?為什麼 Newsfeed 要在意 Story 所需的特定資料?如果資料是由層級結構中 Story 的某個子元件所需要的呢?如果它是一個在許多不同地方使用的元件呢?那麼,每當其資料需求變更時,我們就必須修改許多元件。
為了避免這些問題和許多其他問題,我們可以將 Story 元件的資料需求移至 Story.tsx
中。
我們的方法是將 Story
的資料需求拆分成一個在 Story.tsx
中定義的片段。片段是單獨的 GraphQL 片段,Relay 編譯器會將其拼接成完整的查詢。它們允許每個元件定義自己的資料需求,而無需在執行時付出每個元件執行自己的查詢的代價。
現在讓我們繼續將 Story
的資料需求拆分成片段。
步驟 1 — 定義片段
將以下內容新增至 Story.tsx
(在 src/components
中)的 Story
元件上方
import { graphql } from 'relay-runtime';
const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
createdAt
poster {
name
profilePicture {
url
}
}
image {
url
}
}
`;
請注意,我們已經從查詢中的 topStory
中提取所有選擇,並將它們複製到這個新的片段宣告中。與查詢一樣,片段也有一個名稱 (StoryFragment
),我們稍後會用到它,但它們也有一個 GraphQL 類型 (Story
),它們「基於」該類型。這表示每當我們在圖表中擁有 Story 節點時,都可以使用此片段。
步驟 2 — 展開片段
前往 Newsfeed.tsx
並修改 NewsfeedQuery
,使其看起來像這樣
const NewsfeedQuery = graphql`
query NewsfeedQuery {
topStory {
...StoryFragment
}
}
`;
我們已經將 topStory
內的選擇替換為 StoryFragment
。Relay 編譯器將確保從現在起會提取 Story 的所有資料,而無需變更 Newsfeed
。
步驟 3 — 呼叫 useFragment
您會注意到 Story 現在會呈現一個空的卡片!所有資料都遺失了!Relay 不是應該在從 useLazyLoadQuery()
取得的 story
物件中包含片段選取的欄位嗎?
原因在於 Relay 會將它們隱藏起來。除非元件明確要求特定片段的資料,否則該資料對元件是不可見的。這稱為資料遮罩,並強制元件不要隱含地依賴其他元件的資料相依性,而是在它們自己的片段中宣告它們的所有相依性。這使元件保持獨立和可維護性。
如果沒有資料遮罩,您永遠無法從片段中移除欄位,因為很難驗證其他地方是否有其他元件正在使用它。
若要存取片段選取的資料,我們可以使用名為 useFragment
的 Hook。修改 Story
,使其看起來像這樣
import { useFragment } from 'react-relay';
export default function Story({story}: Props) {
const data = useFragment(
StoryFragment,
story,
);
return (
<Card>
<Heading>{data.title}</Heading>
<PosterByline poster={data.poster} />
<Timestamp time={data.createdAt} />
<Image image={data.image} />
<StorySummary summary={data.summary} />
</Card>
);
}
useFragment
採用兩個引數
- 我們要讀取的片段的GraphQL 標籤字串常值
- 與我們之前使用的相同的story 物件,它來自我們展開片段的 GraphQL 查詢中的位置。這稱為片段索引鍵。
它會傳回該片段選取的資料。
我們已將此處所有 JSX 中的 story
重寫為 data
(useFragment
傳回的資料);請務必在您的元件副本中執行相同操作,否則它將無法運作。
片段索引鍵是 GraphQL 查詢回應中展開片段的位置。例如,假設有 Newsfeed 查詢
query NewsfeedQuery {
topStory {
...StoryFragment
}
}
那麼,如果 queryResult
是由 useLazyLoadQuery
傳回的物件,則 queryResult.topStory
將會是 StoryFragment
的片段索引鍵。
從技術上講,queryResult.topStory
是一個包含一些隱藏欄位的物件,這些欄位會告知 Relay 的 useFragment
在哪裡尋找它需要的資料。片段索引鍵會指定要從哪個節點讀取(這裡只有一個 story,但很快我們就會有多個 story),以及可以讀取哪些欄位(該特定片段選取的欄位)。然後 useFragment
Hook 會從 Relay 的本機資料儲存區中讀取該特定資訊。
我們將在後面的範例中看到,您可以將多個片段展開到查詢中的同一個位置,也可以將片段展開與直接選取的欄位混合使用。
步驟 4 — 片段參照的 TypeScript 類型
若要完成片段化,我們還需要變更 Props
的類型定義,使 TypeScript 知道此元件預期接收片段索引鍵,而不是原始資料。
回想一下,當您將片段展開到查詢(或另一個片段)中時,查詢結果中對應於您展開片段的部分會變成該片段的片段索引鍵。這是您在元件的 props 中傳遞給它的物件,以便為它提供圖表中特定位置以從中讀取片段。
為了使此類型安全,Relay 會產生一個代表該特定片段的片段索引鍵的類型 — 這樣,如果您嘗試使用元件而不將其片段展開到查詢中,您將無法提供符合類型系統的片段索引鍵。以下是我們需要做的變更
import type {StoryFragment$key} from './__generated__/StoryFragment.graphql';
type Props = {
story: StoryFragment$key;
};
完成此操作後,我們有一個 Newsfeed
不再需要關心 Story
需要什麼資料,但仍然可以在它自己的查詢中預先提取該資料。
練習
Story
使用的 PosterByline
元件會呈現發文者的名稱和個人資料圖片。使用相同的步驟來片段化 PosterByline
。您需要
- 在
Actor
上宣告PosterBylineFragment
並指定它需要的欄位 (name
、profilePicture
)。Actor
類型代表可以發佈故事的人員或組織。 - 在
StoryFragment
中的poster
中展開該片段。 - 呼叫
useFragment
以擷取資料。 - 更新 Props 以接受
PosterBylineFragment$key
作為poster
prop。
值得再次執行這些步驟,以熟悉使用片段的機制。這裡有很多部分需要以正確的方式組合在一起。
完成此操作後,讓我們看看片段如何幫助應用程式擴展的基本範例。
在多個位置重複使用片段
片段表示,給定特定類型的某個圖形節點,要從該節點讀取哪些資料。片段鍵指定要從圖形中的哪個節點選取資料。一個可重複使用的元件,指定了一個片段,可以透過傳遞不同的片段鍵,從不同情境下圖形的不同部分擷取資料。
例如,請注意 Image
元件在兩個地方被使用:直接在 Story
內用於故事的縮圖,以及在 PosterByline
內用於海報作者的個人資料圖片。讓我們將 Image
片段化,看看它如何根據其使用位置,從圖形的不同位置選取所需的資料。
步驟 1 — 定義片段
開啟 Image.tsx
並加入一個片段定義
import { graphql } from 'relay-runtime';
const ImageFragment = graphql`
fragment ImageFragment on Image {
url
}
`;
步驟 2 — 擴展片段
回到 StoryFragment
和 PosterBylineFragment
,並在每個使用資料的 Image
元件的地方,將 ImageFragment
擴展到其中
- Story.tsx
- PosterByline.tsx
const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
postedAt
poster {
...PosterBylineFragment
}
thumbnail {
...ImageFragment
}
}
`;
const PosterBylineFragment = graphql`
fragment PosterBylineFragment on Actor {
name
profilePicture {
...ImageFragment
}
}
`;
步驟 3 — 呼叫 useFragment
修改 Image
元件以使用其片段讀取欄位,並修改其 Props 以接受片段鍵
import { useFragment } from 'react-relay';
import type { ImageFragment$key } from "./__generated__/ImageFragment.graphql";
type Props = {
image: ImageFragment$key;
...
};
function Image({image}: Props) {
const data = useFragment(ImageFragment, image);
return <img key={data.url} src={data.url} ... />
}
步驟 4 — 修改一次,處處受益
現在我們已經將 Image 的資料需求片段化,並將它們放在元件內,我們可以在不修改任何使用它的元件的情況下,向 Image 添加新的資料依賴項。
例如,讓我們為 Image
元件新增一個用於協助工具的 altText
標籤。
編輯 ImageFragment
如下
const ImageFragment = graphql`
fragment ImageFragment on Image {
url
altText
}
`;
現在,無需編輯 Story、Newsfeed 或任何其他元件,我們查詢中的所有圖片都會擷取 alt 文字。所以我們只需要修改 Image
以使用新的欄位
function Image({image}) {
// ...
<img
alt={data.altText}
//...
}
現在故事縮圖和海報作者的個人資料圖片都將具有替代文字。(您可以使用瀏覽器的「元素檢查器」來驗證這一點。)
您可以想像當您的程式碼庫變得更大時,這有多大的好處。每個元件都是獨立的,無論它在多少個地方使用!即使一個元件在數百個地方使用,您也可以隨意地在其資料依賴項中新增或移除欄位。這是 Relay 幫助您隨應用程式規模擴展的主要方法之一。
片段是 Relay 應用程式的建構區塊。因此,許多 Relay 功能都基於片段。我們將在接下來的章節中介紹其中一些。
片段參數和欄位參數
目前,即使 Image
元件將以較小的尺寸顯示,它仍會以全尺寸擷取圖片。這效率不高!Image
元件接受一個 prop,用於指定顯示圖片的尺寸,因此它由使用 Image
的元件控制。我們希望以類似的方式,讓使用 Image
的元件在其片段中指定要擷取哪種尺寸的圖片。
GraphQL 欄位可以接受參數,以向伺服器提供額外的資訊來滿足我們的請求。例如,Image
類型上的 url
欄位接受 height
和 width
參數,伺服器會將這些參數納入 URL 中 — 如果我們有這個片段
fragment Example1 on Image {
url
}
我們可能會取得像是 /images/abcde.jpeg
的 URL
— 而如果我們有這個片段
fragment Example2 on Image {
url(height: 100, width: 100)
}
我們可能會取得像是 /images/abcde.jpeg?height=100&width=100
的 URL
現在,我們當然不希望將特定的尺寸硬式編碼到 ImageFragment
中,因為我們希望 Image
元件在不同情境下擷取不同的尺寸。為此,我們可以讓 ImageFragment
接受片段參數,以便父元件可以指定應擷取多大的圖片。這些片段參數然後可以作為欄位參數傳遞到特定的欄位(在本例中為 url
)。
為此,請編輯 ImageFragment
如下
const ImageFragment = graphql`
fragment ImageFragment on Image
@argumentDefinitions(
width: {
type: "Int",
defaultValue: null
}
height: {
type: "Int",
defaultValue: null
}
)
{
url(
width: $width,
height: $height
)
altText
}
`;
讓我們分解一下
- 我們已將
@argumentDefinitions
指令新增至片段宣告。這表示片段接受哪些參數。對於每個參數,我們提供- 參數的名稱
- 其類型(可以是任何 GraphQL 純量類型)
- 可選的 預設值 — 在這種情況下,預設值為 null,這允許我們以其固有尺寸擷取圖片。如果未提供預設值,則每次使用該片段時都必須提供該參數。
- 然後,我們透過將片段參數用作變數來填入 GraphQL 欄位的參數。在這裡,欄位參數和片段參數具有相同的名稱(通常情況下如此),但請注意:
width:
是欄位參數,而$width
是由片段參數建立的變數。
現在,片段接受一個參數,並透過其選取的一個欄位將該參數傳遞給伺服器。
深入探討:GraphQL 指令
片段參數的語法可能看起來相當笨拙。這是因為它基於指令,這是一種用於擴充 GraphQL 語言的系統。在 GraphQL 中,任何以 @
開頭的符號都是指令。它們的含義不是由 GraphQL 規格定義,而是由特定的客戶端或伺服器實作決定。
Relay 定義了多個指令來支援其功能 — 例如片段參數。這些指令不會傳送至伺服器,而是會在建置時向 Relay 編譯器提供指示。
GraphQL 規格實際上定義了三個指令的含義
@deprecated
用於結構描述定義中,並將欄位標記為已棄用。@include
和@skip
可用於使欄位的包含條件化。
除此之外,GraphQL 伺服器可以將其他指令指定為其結構描述的一部分。Relay 也有其自己的建置時指令,這讓我們可以在不變更其語法的情況下稍微擴充語言。
步驟 2
現在,使用 Image
的不同片段可以傳入每個圖片的適當尺寸
- Story.tsx
- PosterByline.tsx
const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
postedAt
poster {
...PosterBylineFragment
}
thumbnail {
...ImageFragment @arguments(width: 400)
}
}
`;
const PosterBylineFragment = graphql`
fragment PosterBylineFragment on Actor {
name
profilePicture {
...ImageFragment @arguments(width: 60, height: 60)
}
}
`;
現在,如果您查看我們的應用程式下載的圖片,您會看到它們是較小的尺寸,從而節省了網路頻寬。請注意,雖然我們為片段參數的值使用了整數文字,但我們也可以使用在執行階段提供的變數,這將在後面的章節中看到。
欄位參數 (例如 url(height: 100)
) 是 GraphQL 本身的功能,而片段參數 (例如 @argumentDefinitions
和 @arguments
) 是 Relay 特有的功能。Relay 編譯器會在將片段組合到查詢中時處理這些片段參數。
摘要
片段是 Relay 使用 GraphQL 的最顯著方面。我們建議每個顯示資料並關心該資料語義的元件(因此不只是排版或格式化元件)都使用 GraphQL 片段來宣告其資料依賴項。
- 片段可協助您擴展:無論元件在多少個地方使用,您都可以在單一位置更新其資料依賴項。
- 片段資料需要使用
useFragment
讀取。 useFragment
接受一個片段鍵,用於指定要從圖形的哪個位置讀取。- 片段鍵來自 GraphQL 回應中擴展該片段的位置。
- 片段可以定義在擴展點使用的參數。這允許它們根據使用的每個情況進行調整。
我們將重新探討片段的許多其他功能,例如如何在不重新擷取整個查詢的情況下重新擷取單個片段的內容。但是,首先,讓我們透過學習陣列使這個新聞提要應用程式更像新聞提要。