13. 解析器链
5m

概述

我们可以 房源的便利设施,但只能通过 listing(id: ID)不是 通过 featuredListings。发生了什么事?

在本课中,我们将

  • 了解
  • 了解 parent

检查数据源响应

让我们再次检查来自 GET /featured-listings 端点的响应。打开一个新的浏览器标签页,并粘贴下面的 URL。

GET /featured-listings 端点
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 listing
return listings;
}

嗯,这会起作用;但它意味着 每次 我们 featuredListings,我们都会 总是 向 REST API 发起额外的网络调用,无论 是否请求了房源的 amenities

因此,我们将使用 链。

沿着解析器链

一个 解析器链 是解析特定 时调用 函数的顺序。它可以包含顺序路径以及并行分支。

让我们从我们的项目中举个例子。这个 GetListing 检索房源的标题。

query GetListing($listingId: ID!) {
listing(id: $listingId) {
title
}
}

当解析这个 时, 将首先调用 Query.listing() 函数,该函数返回一个 Listing 类型,然后是 Listing.title() ,它返回一个 String 类型并结束链。

Resolver chain in a diagram

注意: 我们不需要定义一个 独立的 Listing.title,因为 title 属性可以直接从 Query.listing 返回的实例中返回。

每个 将其返回值传递给下一个函数,使用解析器的 parent 。因此:

记住,一个 可以访问许多参数。到目前为止,我们已经使用了 contextValue (访问我们的 ListingAPI )和 args (获取房源的 id)。 parent 是另一个这样的参数!

在上面的示例中,Query.listing() 返回一个 Listing 对象,Listing.title() 将接收该对象作为其 parent

让我们看一下另一个

query GetListingAmenities($listingId: ID!) {
listing(id: $listingId) {
title
amenities {
name
}
}
}

这次,我们添加了更多 ,并请求了每个房源的便利设施列表,特别是它们的 name 值。

我们的 链增长,添加了一个并行分支。

Resolver chain in a diagram

因为 Listing.amenities 返回一个可能包含多个便利设施的列表,所以这个 可能运行多次来检索每个便利设施的名称。

沿着 的轨迹,Listing.amenities() 可以访问 Listing 作为 parent,就像 Amenity.name() 可以访问 Amenity 对象作为 parent

如果我们的 没有包含 amenities (就像我们展示的第一个例子),那么 Listing.amenities() 将永远不会被调用!

实现 Listing.amenities 解析器

到目前为止,我们已经为 函数定义了只存在于我们 上的 Query 类型。但是我们实际上可以为模式中的任何 定义一个 函数。

让我们创建一个 函数,它的 唯一责任 是为给定的 Listing 对象返回 amenities 数据。

进入 resolvers.ts 。在这里,我们将在 Query 对象下方添加一个新条目,称为 Listing

resolvers.ts
export const resolvers: Resolvers = {
Query: {
// query resolvers, featuredListings and listing
},
Listing: {
// TODO
},
};

Listing 对象内,我们将定义一个新的 函数,称为 amenities 。我们会立即返回 null ,以便 TypeScript 在我们探索函数参数时继续编译。

resolvers.ts
Listing: {
amenities: (parent, args, contextValue, info) => {
return null;
}
},

我们知道 parent 的值——通过遵循 链,我们知道这个解析器将接收它尝试返回 amenitiesListing 对象。(我们不需要 argsinfo 参数,因此我们将用 _ 替换 args 并从函数签名中删除 info 。)

让我们输出 parent 的值。

resolvers.ts
Listing: {
amenities: (parent, _, contextValue) => {
console.log(parent);
return null;
}
},

然后我们将回到 Sandbox 并运行一个 来调用这个 函数。

query GetFeaturedListings {
featuredListings {
id
title
description
amenities {
id
name
category
}
}
}

当我们运行此 时,响应 面板将显示 "Cannot return null for non-nullable field Listing.amenities." ,但这没关系,我们更感兴趣的是现在调查 parent

从我们在终端中输出的值可以看出,parent 是我们查询的 Listing 对象——以及它的所有属性。现在在 Listing.amenities 中,让我们清理我们的日志和返回语句。然后我们将解构 parentidamenities 属性,以及 contextValuedataSources

resolvers.ts
Listing: {
amenities: ({ id, amenities }, _, { dataSources }) => {
// TODO
}
},

我们需要我们的 Listing.amenities 处理两种情况。

  1. 如果我们查询的是 单个 列表,我们已经在 parent 上获得了完整的便利设施数据。在这种情况下,我们可以直接返回 amenities
  2. 如果我们的 parent 来自 Query.featuredListings ,但是,我们只拥有便利设施 ID 的数组。在这种情况下,我们需要向 REST API 发出后续请求!
resolvers.ts
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

listing-api.ts
import { Listing, Amenity } from "../types";

我们将为我们的类添加一个新方法:getAmenities 。此方法接受一个参数,listingId ,它是 string 类型,并返回一个 Promise ,它返回一个 Amenity 类型的列表。

listing-api.ts
getAmenities(listingId: string): Promise<Amenity[]> {
// TODO
}

在函数内部,我们将调用 this.get ,它返回一个 Amenity 类型的列表,并将我们需要的端点传入。

listing-api.ts
getAmenities(listingId: string): Promise<Amenity[]> {
return this.get<Amenity[]>(`listings/${listingId}/amenities`);
}

完成解析器

然后,回到我们的 Listing.amenities 中,我们将调用 listingAPI.getAmenities 方法,并将我们的 Listingid 传入。

resolvers.ts
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 函数。

resolvers.ts
import { validateFullAmenities } from "./helpers";

在我们的 Listing.amenities 中,我们将首先检查我们的 amenities 是否包含所有属性。如果是,我们将直接返回它们。否则,我们可以发出后续请求来获取额外的便利设施数据。

amenities: ({ id, amenities }, _, { dataSources }) => {
return validateFullAmenities(amenities)
? amenities
: dataSources.listingAPI.getAmenities(id);
},

尝试我们的查询

为了查看我们的 ListingAPIgetAmenities 方法何时被调用来获取后续的便利设施数据,让我们在 listing-api.ts 中的方法内部添加一个 console 日志。

listing-api.ts
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 {
id
name
}
}
}

变量 面板中:

{
"listingId": "listing-1"
}

当我们运行这个 时,我们应该看到单个列表及其便利设施的响应没有改变。我们的 只是从 parent 返回 amenities

现在让我们尝试对我们的特色列表进行相同的 。在 Explorer 中打开一个新标签页,并粘贴以下查询。

query GetFeaturedListings {
featuredListings {
id
title
description
amenities {
id
name
category
}
}
}

现在,当我们运行这个 时,我们仍然可以看到便利设施数据 - 但是我们的终端显示已经发出了三个额外的请求来填充每个房源的便利设施集合的剩余属性!

Making a follow-up call for amenities with listing-1
Making a follow-up call for amenities with listing-2
Making a follow-up call for amenities with listing-3

练习

使用以下模式和 来回答多项选择题。

一个示例模式
type Query {
featuredPlanets: [Planet!]!
}
type Planet {
name: String!
galaxy: Galaxy!
}
type Galaxy {
name: String!
totalPlanets: Int!
dateDiscovered: String
}
一个示例查询操作
query GetFeaturedPlanetsGalaxies {
featuredPlanets {
galaxy {
name
}
}
}
以下哪个选项准确地描述了上面 GetFeaturedPlanetsGalaxies 查询的解析器链?

关键要点

  • 一个 链是解析特定 时调用解析器函数的顺序。

接下来

对查询有信心了吗?是时候探索 的另一面:

上一个

分享您对本课的疑问和意见

本课程目前处于

beta
.您的反馈将帮助我们改进!如果您卡住或困惑,请告诉我们,我们会帮助您。所有评论都是公开的,必须遵守 Apollo 行为准则。请注意,已解决或已处理的评论可能会被删除。

您需要一个 GitHub 帐户才能在下方发布。没有? 改为在我们的 Odyssey 论坛中发布。