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

片段

片段是 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 編譯器會將其拼接成完整的查詢。它們允許每個元件定義自己的資料需求,而無需在執行時付出每個元件執行自己的查詢的代價。

The Relay compiler combines the fragment into the place it&#39;s spread

現在讓我們繼續將 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 重寫為 datauseFragment 傳回的資料);請務必在您的元件副本中執行相同操作,否則它將無法運作。

片段索引鍵是 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 並指定它需要的欄位 (nameprofilePicture)。Actor 類型代表可以發佈故事的人員或組織。
  • StoryFragment 中的 poster 中展開該片段。
  • 呼叫 useFragment 以擷取資料。
  • 更新 Props 以接受 PosterBylineFragment$key 作為 poster prop。

值得再次執行這些步驟,以熟悉使用片段的機制。這裡有很多部分需要以正確的方式組合在一起。

完成此操作後,讓我們看看片段如何幫助應用程式擴展的基本範例。


在多個位置重複使用片段

片段表示,給定特定類型的某個圖形節點,要從該節點讀取哪些資料。片段鍵指定要從圖形中的哪個節點選取資料。一個可重複使用的元件,指定了一個片段,可以透過傳遞不同的片段鍵,從不同情境下圖形的不同部分擷取資料。

例如,請注意 Image 元件在兩個地方被使用:直接在 Story 內用於故事的縮圖,以及在 PosterByline 內用於海報作者的個人資料圖片。讓我們將 Image 片段化,看看它如何根據其使用位置,從圖形的不同位置選取所需的資料。

Fragment can be used in multiple places

步驟 1 — 定義片段

開啟 Image.tsx 並加入一個片段定義

import { graphql } from 'relay-runtime';

const ImageFragment = graphql`
fragment ImageFragment on Image {
url
}
`;

步驟 2 — 擴展片段

回到 StoryFragmentPosterBylineFragment,並在每個使用資料的 Image 元件的地方,將 ImageFragment 擴展到其中

const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
postedAt
poster {
...PosterBylineFragment
}
thumbnail {
...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 幫助您隨應用程式規模擴展的主要方法之一。

Field added to one fragment is added in all places it&#39;s used

片段是 Relay 應用程式的建構區塊。因此,許多 Relay 功能都基於片段。我們將在接下來的章節中介紹其中一些。


片段參數和欄位參數

目前,即使 Image 元件將以較小的尺寸顯示,它仍會以全尺寸擷取圖片。這效率不高!Image 元件接受一個 prop,用於指定顯示圖片的尺寸,因此它由使用 Image 的元件控制。我們希望以類似的方式,讓使用 Image 的元件在其片段中指定要擷取哪種尺寸的圖片。

GraphQL 欄位可以接受參數,以向伺服器提供額外的資訊來滿足我們的請求。例如,Image 類型上的 url 欄位接受 heightwidth 參數,伺服器會將這些參數納入 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 的不同片段可以傳入每個圖片的適當尺寸

const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
postedAt
poster {
...PosterBylineFragment
}
thumbnail {
...ImageFragment @arguments(width: 400)
}
}
`;

現在,如果您查看我們的應用程式下載的圖片,您會看到它們是較小的尺寸,從而節省了網路頻寬。請注意,雖然我們為片段參數的值使用了整數文字,但我們也可以使用在執行階段提供的變數,這將在後面的章節中看到。

欄位參數 (例如 url(height: 100)) 是 GraphQL 本身的功能,而片段參數 (例如 @argumentDefinitions@arguments) 是 Relay 特有的功能。Relay 編譯器會在將片段組合到查詢中時處理這些片段參數。


摘要

片段是 Relay 使用 GraphQL 的最顯著方面。我們建議每個顯示資料並關心該資料語義的元件(因此不只是排版或格式化元件)都使用 GraphQL 片段來宣告其資料依賴項。

  • 片段可協助您擴展:無論元件在多少個地方使用,您都可以在單一位置更新其資料依賴項。
  • 片段資料需要使用 useFragment 讀取。
  • useFragment 接受一個片段鍵,用於指定要從圖形的哪個位置讀取。
  • 片段鍵來自 GraphQL 回應中擴展該片段的位置。
  • 片段可以定義在擴展點使用的參數。這允許它們根據使用的每個情況進行調整。

我們將重新探討片段的許多其他功能,例如如何在不重新擷取整個查詢的情況下重新擷取單個片段的內容。但是,首先,讓我們透過學習陣列使這個新聞提要應用程式更像新聞提要。