React + TypeScript: Apollo Client中的GraphQL最佳实践——命名操作和GraphQL变量等
Apollo Client是用于React的状态管理库。它可以通过GraphQL处理本地和远程数据。本文基于官方网站上的”GraphQL query best practices – Operation naming, GraphQL variables, and more”,对操作命名和GraphQL变量的使用等最佳实践进行了解释。前提是您已经掌握了使用Apollo Client进行查询的基础知识(如果还没有,请先阅读”React + TypeScript: 使用Apollo Client的GraphQL查询”)。这并不是文档的翻译,而是用日语重新解释。省略了原文的部分,并在需要补充的地方进行了补充以提高理解。
遵循下面所述的最佳实践,当创建查询或进行更改时,可以最大程度地利用GraphQL和Apollo工具。
对于每个操作,都需要为其命名。
以下的代码示例中的两个查询将获取相同的数据。
# 推奨 ✅
query GetBooks {
books {
title
}
}
# 非推奨 ❌
query {
books {
title
}
}
第一个查询被命名为GetBooks,但是第二个查询没有名字。
在应用程序的GraphQL操作中,必须给每个操作命名。其优点如下:
在应用程序的GraphQL操作中,每个操作都必须命名。以下是其优点:
-
- 自分とチームの中で、それぞれの操作の目的が明確になります。
-
- ひとつのクエリドキュメントで複数の操作を組み合わせたときに、予期しないエラーが避けられます(操作に名前がないと単体でしか確認できません)。
-
- クライアントとサーバーのコードの両方でデバッグ出力が改善します。どの操作が問題を引き起こしているのか正確に絞りやすくなるからです。
Apollo Studioの提供する便利な操作レベルのメトリクスを使うには、操作に名前がなければなりません。
使用GraphQL变量传递参数
以下是两个查询的代码示例,它们都可以根据参数id的值获取Dog对象。
# 推奨 ✅
query GetDog($dogId: ID!) {
dog(id: $dogId) {
name
breed
}
}
# 非推奨 ❌
query GetDog {
dog(id: "5") {
name
breed
}
}
第一个查询将变量($dogId)传递给dog字段的必需参数值,也就是说,可以使用此查询获取具有任意ID的Dog对象。这样可以大大方便重复使用。
使用Query(或者useMutation)时传递变量值(id)的方式如下示例所示。
const GET_DOG = gql`
query GetDog($dogId: ID!) {
dog(id: $dogId) {
name
breed
}
}
`;
function Dog({ id }) {
const { loading, error, data } = useQuery(GET_DOG, {
variables: {
dogId: id
},
});
// ...コンポーネントのレンダリング...
}
GraphQL引数的直接描述存在的问题。
直接写入参数不仅难以重复利用,还相比于变量具有以下劣势。
缓存效率降低了。
当在两个相同的查询中直接写入参数值时,GraphQL服务器会将其视为完全不同的操作,并从缓存中获取结果。如果缓存有效,服务器将不再重复之前的分析和验证,从而提高性能。
服务器端的缓存在联合网关中也发挥着自动持久查询和查询计划等功能的作用。如果直接写入参数,将无法获得这些功能的性能。有用的缓存空间将被占用。
信息隐私的下降
GraphQL的值可能包含敏感信息,例如访问令牌和用户个人信息。如果这些信息包含在查询字符串中,它们将被与其他查询字符串一起缓存。
变量值不包含在查询字符串中。此外,您还可以指定是否将哪些变量值(如果有)包含在Apollo Studio的指标报告中。
只在所需的地方获取必要的数据。
GraphQL相对于传统的REST API的一个显著优点是它支持声明式数据获取。每个组件只需准确地查询所需的字段,以进行渲染(并且应该如此)。它不会发送任何多余的数据到网络上去。
对此,我们假设根组件执行了一个巨大的查询,以获取所有子组件的数据。甚至可能会执行尚未渲染的组件的查询。这可能会导致响应延迟。这样一来,服务器端的响应缓存将无法有效地重新利用查询结果。
大多数情况下,像下面的查询应该被分成多个部分,并分散到适当的组件中。
# 非推奨 ❌
query GetGlobalStatus {
stores {
id
name
address {
street
city
}
employees {
id
}
manager {
id
}
}
products {
id
name
price {
amount
currency
}
}
employees {
id
role
name {
firstName
lastName
}
store {
id
}
}
offers {
id
products {
id
}
discount {
discountType
amount
}
}
}
-
- つねに一緒にレンダリングされるコンポーネントのまとまりがあるときは、フラグメントを使いましょう。単一のクエリの構造を複数のコンポーネントの間で分散できます(「フラグメントの配置」参照)。
- リストフィールドを問い合わせるとき、返される項目数がコンポーネントのレンダリングすべき数より多い場合には、フィールドはページ割りしましょう。
使用片段化技术来封装相关字段的组合。
GraphQL的片段是一组可在多个操作中共享的字段。
# 推奨 ✅
fragment NameParts on Person {
title
firstName
middleName
lastName
}
假设在应用程序的多个查询中需要个人的全名。NameParts片段可以帮助保持这些查询的一致性,使其易读且简短。
# 推奨 ✅
query GetAttendees($eventId: ID!) {
attendees(id: $eventId) {
id
rsvp
...NameParts # NamePartsフラグメントからすべてのフィールドを読み込む
}
}
避免使用过分或不合逻辑的片段。
如果片段使用过多,查询就会变得难以阅读。
# 要注意 ⚠️
query GetAttendees($eventId: ID!) {
attendees(id: $eventId) {
id
rsvp
...NameParts
profile {
...VisibilitySettings
events {
...EventSummary
}
avatar {
...ImageDetails
}
}
}
}
此外,只有在共享逻辑意义关系的字段集合中定义为片段。并不是因为一些查询恰好共享特定字段,就应该将其作为片段。
# 推奨 ✅
fragment NameParts on Person {
title
firstName
middleName
lastName
}
# 非推奨 ❌
fragment SharedFields on Country {
population
neighboringCountries {
capital
rivers {
name
}
}
}
全球数据和用户特定数据需要分别查询。
根据不同的领域,无论是哪个用户提出的查询,返回的数据可能完全相同。
# 周期表のすべての要素を返す
query GetAllElements {
elements {
atomicNumber
name
symbol
}
}
然而,根据执行查询的用户不同,也可能返回不同的数据字段。
# 現在のユーザーのドキュメントを返す
query GetMyDocuments {
myDocuments {
id
title
url
updatedAt
}
}
为了提高服务器端响应缓存的性能,尽可能根据字段的全局性或用户特定性进行区分和获取。通过这样做,像预先获取全部元素(GetAllElements)这样的查询可以缓存单个响应,而执行获取我的文档(GetMyDocuments)则可以分别按用户进行缓存。
为了生成指标报告,设置应用程序的名称和版本(付费功能)。
[注意] 此功能对于订阅Apollo Studio付费计划的组织非常有效。但是,它也对所有其他未订阅的应用程序有用。
ApolloClient构造函数的参数对象选项,为name和version。
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache(),
name: 'MarketingSite',
version: '1.2'
});
设置这些值后,Apollo Client会自动添加它们作为HTTP头。
-
- apolllographql-client-name
- apolllographql-client-version
当在Apollo Studio中设置度量报告时,Apollo Server会将操作追踪中的名称和版本添加到Studio中进行报告。这样可以实现客户端的分段度量。