概述
我们可以查询 房源的便利设施,但只能通过 listing(id: ID)
根 字段,不是 通过 featuredListings
。发生了什么事?
在本课中,我们将
- 了解 解析器 链
- 了解
parent
参数 的 解析器
检查数据源响应
让我们再次检查来自 GET /featured-listings
端点的响应。打开一个新的浏览器标签页,并粘贴下面的 URL。
https://rt-airlock-services-listing.herokuapp.com/featured-listings
我们得到的数组包含我们期望的房源对象;但我们注意到每个房源对象中的一个属性不同!从 /featured-listings
端点,房源的 "amenities"
列表是一个数组,其中包含 仅 每个便利设施的标识符。没有额外数据!
这在 REST API 中是一种常见模式。想象一下,如果每个房源的便利设施列表包含 完整 每个便利设施的数据。为了获得一个房源列表 和 每个房源的完整便利设施列表,这将导致非常大的响应!如果端点决定返回比我们已经拥有的三个房源更多的精选房源,问题会加剧。
为了确保我们能够返回精选房源 以及 他们的便利设施,我们需要针对每个房源向 REST API 额外进行一次调用。在这种情况下,调用一个新的端点: GET /listings/{listing_id}/amenities
。
下一个问题是:我们在代码中的哪里进行这个调用?
处理缺少的 amenities
为了处理我们缺少的 amenities
,我们可以 更新 我们的 featuredListings
解析器 函数。我们可以为其添加一些额外的逻辑,以访问 GET /listings/{listing_id}/amenities
来获取这些额外的数据。
async getFeaturedListings(): Promise<Listing[]> {const listings = await this.get<Listing[]>("featured-listings");// for each listing ID, request this.get<Amenity[]>(`listings/{id}/amenities`)// Then map each set of amenities back to its listingreturn listings;}
嗯,这会起作用;但它意味着 每次 我们 查询 featuredListings
,我们都会 总是 向 REST API 发起额外的网络调用,无论 查询 是否请求了房源的 amenities
。
因此,我们将使用 解析器 链。
沿着解析器链
一个 解析器链 是解析特定 GraphQL 操作 时调用 解析器 函数的顺序。它可以包含顺序路径以及并行分支。
让我们从我们的项目中举个例子。这个 GetListing
操作 检索房源的标题。
query GetListing($listingId: ID!) {listing(id: $listingId) {title}}
当解析这个 操作 时,GraphQL 服务器 将首先调用 Query.listing()
解析器 函数,该函数返回一个 Listing
类型,然后是 Listing.title()
解析器,它返回一个 String
类型并结束链。
注意: 我们不需要定义一个 独立的 解析器 Listing.title
,因为 title
属性可以直接从 Query.listing
返回的实例中返回。
每个 解析器 将其返回值传递给下一个函数,使用解析器的 parent
参数。因此: 解析器 链!
记住,一个 解析器 可以访问许多参数。到目前为止,我们已经使用了 contextValue
(访问我们的 ListingAPI
数据源)和 args
(获取房源的 id
)。 parent
是另一个这样的参数!
在上面的示例中,Query.listing()
返回一个 Listing
对象,Listing.title()
解析器 将接收该对象作为其 parent
参数。
让我们看一下另一个 GraphQL 操作。
query GetListingAmenities($listingId: ID!) {listing(id: $listingId) {titleamenities {name}}}
这次,我们添加了更多 字段,并请求了每个房源的便利设施列表,特别是它们的 name
值。
我们的 解析器 链增长,添加了一个并行分支。
因为 Listing.amenities
返回一个可能包含多个便利设施的列表,所以这个 解析器 可能运行多次来检索每个便利设施的名称。
沿着 解析器 的轨迹,Listing.amenities()
可以访问 Listing
作为 parent
,就像 Amenity.name()
可以访问 Amenity
对象作为 parent
。
如果我们的 操作 没有包含 amenities
字段 (就像我们展示的第一个例子),那么 Listing.amenities()
解析器 将永远不会被调用!
实现 Listing.amenities
解析器
到目前为止,我们已经为 解析器 函数定义了只存在于我们 字段 上的 Query
类型。但是我们实际上可以为模式中的任何 字段 定义一个 解析器 函数。
让我们创建一个 解析器 函数,它的 唯一责任 是为给定的 Listing
对象返回 amenities
数据。
进入 resolvers.ts
。在这里,我们将在 Query
对象下方添加一个新条目,称为 Listing
。
export const resolvers: Resolvers = {Query: {// query resolvers, featuredListings and listing},Listing: {// TODO},};
在 Listing
对象内,我们将定义一个新的 解析器 函数,称为 amenities
。我们会立即返回 null
,以便 TypeScript 在我们探索函数参数时继续编译。
Listing: {amenities: (parent, args, contextValue, info) => {return null;}},
我们知道 parent
参数 的值——通过遵循 解析器 链,我们知道这个解析器将接收它尝试返回 amenities
的 Listing
对象。(我们不需要 args
或 info
参数,因此我们将用 _
替换 args
并从函数签名中删除 info
。)
让我们输出 parent
的值。
Listing: {amenities: (parent, _, contextValue) => {console.log(parent);return null;}},
然后我们将回到 Sandbox 并运行一个 查询 来调用这个 解析器 函数。
query GetFeaturedListings {featuredListings {idtitledescriptionamenities {idnamecategory}}}
当我们运行此 操作 时,响应 面板将显示 "Cannot return null for non-nullable field Listing.amenities."
,但这没关系,我们更感兴趣的是现在调查 parent
。
从我们在终端中输出的值可以看出,parent
是我们查询的 Listing
对象——以及它的所有属性。现在在 Listing.amenities
解析器 中,让我们清理我们的日志和返回语句。然后我们将解构 parent
的 id
和 amenities
属性,以及 contextValue
的 dataSources
。
Listing: {amenities: ({ id, amenities }, _, { dataSources }) => {// TODO}},
我们需要我们的 Listing.amenities
解析器 处理两种情况。
- 如果我们查询的是 单个 列表,我们已经在
parent
参数 上获得了完整的便利设施数据。在这种情况下,我们可以直接返回amenities
。 - 如果我们的 解析器 的
parent
参数 来自Query.featuredListings
,但是,我们只拥有便利设施 ID 的数组。在这种情况下,我们需要向 REST API 发出后续请求!
Listing: {amenities: ({ id, amenities }, _, { dataSources }) => {// If `amenities` contains full-fledged Amenity objects, return them// Otherwise make a follow-up request to /listings/{listing_id}/amenities}},
让我们构建我们新的 ListingAPI
方法来获取便利设施数据,然后我们将回到这里处理这两个路径。
getAmenities
方法
在 listing-api.ts
中,让我们引入 Amenity
类型,它来自 types.ts
。
import { Listing, Amenity } from "../types";
我们将为我们的类添加一个新方法:getAmenities
。此方法接受一个参数,参数 为 listingId
,它是 string
类型,并返回一个 Promise
,它返回一个 Amenity
类型的列表。
getAmenities(listingId: string): Promise<Amenity[]> {// TODO}
在函数内部,我们将调用 this.get
,它返回一个 Amenity
类型的列表,并将我们需要的端点传入。
getAmenities(listingId: string): Promise<Amenity[]> {return this.get<Amenity[]>(`listings/${listingId}/amenities`);}
完成解析器
然后,回到我们的 Listing.amenities
解析器 中,我们将调用 listingAPI.getAmenities
方法,并将我们的 Listing
的 id
传入。
amenities: ({ id, amenities }, _, { dataSources }) => {return dataSources.listingAPI.getAmenities(id);};
我们已经处理了需要请求后续便利设施数据的情况。现在,让我们更新我们的 解析器 首先检查我们是否 已经 在 parent
参数 上获得了完整的便利设施数据。
我们提供了一个帮助我们完成此操作的工具。进入 src/helpers.ts
并取消注释那里的代码。我们将使用此处导出的 validateFullAmenities
函数:它接受一个 amenities
参数(Amenity[]
类型),并检查提供的数组中是否至少有一些对象包含 name
属性。
export const validateFullAmenities = (amenityList: Amenity[]) =>amenityList.some(hasOwnPropertyName);
注意: 我们选择了检查是否存在 name
属性(来确定我们是否处理的是完整的便利设施数据),但我们也可以使用 category
属性来执行此检查。
回到 resolvers.ts
中,让我们导入 validateFullAmenities
函数。
import { validateFullAmenities } from "./helpers";
在我们的 Listing.amenities
解析器 中,我们将首先检查我们的 amenities
是否包含所有属性。如果是,我们将直接返回它们。否则,我们可以发出后续请求来获取额外的便利设施数据。
amenities: ({ id, amenities }, _, { dataSources }) => {return validateFullAmenities(amenities)? amenities: dataSources.listingAPI.getAmenities(id);},
尝试我们的查询
为了查看我们的 ListingAPI
的 getAmenities
方法何时被调用来获取后续的便利设施数据,让我们在 listing-api.ts
中的方法内部添加一个 console 日志。
getAmenities(listingId: string): Promise<Amenity[]> {console.log("Making a follow-up call for amenities with ", listingId);return this.get<Amenity[]>(`listings/${listingId}/amenities`)}
现在我们可以返回到 Explorer,地址为https://127.0.0.1:4000 并尝试几个查询。
首先,一个 查询 ,用于单个列表。
query GetListing($listingId: ID!) {listing(id: $listingId) {amenities {idname}}}
在 变量 面板中:
{"listingId": "listing-1"}
当我们运行这个 查询 时,我们应该看到单个列表及其便利设施的响应没有改变。我们的 解析器 只是从 parent
参数 返回 amenities
。
现在让我们尝试对我们的特色列表进行相同的 查询 。在 Explorer 中打开一个新标签页,并粘贴以下查询。
query GetFeaturedListings {featuredListings {idtitledescriptionamenities {idnamecategory}}}
现在,当我们运行这个 查询时,我们仍然可以看到便利设施数据 - 但是我们的终端显示已经发出了三个额外的请求来填充每个房源的便利设施集合的剩余属性!
Making a follow-up call for amenities with listing-1Making a follow-up call for amenities with listing-2Making a follow-up call for amenities with listing-3
练习
使用以下模式和 GraphQL 查询来回答多项选择题。
type Query {featuredPlanets: [Planet!]!}type Planet {name: String!galaxy: Galaxy!}type Galaxy {name: String!totalPlanets: Int!dateDiscovered: String}
query GetFeaturedPlanetsGalaxies {featuredPlanets {galaxy {name}}}
GetFeaturedPlanetsGalaxies
查询的解析器链?关键要点
- 一个 解析器链是解析特定 GraphQL 操作时调用解析器函数的顺序。
接下来
对查询有信心了吗?是时候探索 GraphQL 的另一面: 突变。
分享您对本课的疑问和意见
本课程目前处于
您需要一个 GitHub 帐户才能在下方发布。没有? 改为在我们的 Odyssey 论坛中发布。