12. 解析器链
5m

概述

我们可以 播放列表的曲目,但只能通过 playlist(id: ID)而不是 通过 featuredPlaylists。发生了什么事?

在本课中,我们将

  • 了解
  • 了解 parent

检查数据源响应

让我们检查一下 GET /browse/featured-playlists 端点的响应。看起来我们确实可以访问 tracks 属性,它位于 playlist.items.tracks 下。

"tracks": {
"href": "string",
"total": 0
}

注意:或者,你可以在 SpotifyService 类中,按照类型和属性的轨迹找到相同的信息。

我们没有得到一个曲目对象的列表(类似于之前的 GET /playlists/{playlist_id} 端点返回的列表),而是得到一个具有两个属性的单个对象: total,表示可用的总曲目数量,以及 href,表示我们 可以 检索完整曲目对象列表的端点的 URL。

这是 REST API 中常见的模式。想象一下,如果响应 确实 包含了完整的曲目对象列表。为了包含播放列表的列表 以及 每个播放列表的曲目列表,这将导致非常大的响应。

相反,我们需要对 REST API 再进行一次额外的调用。在本例中,是 GET /playlists/{playlist_id}/tracks 端点。

下一个问题是:我们在代码中的哪个位置进行该调用?

检查查询

让我们退一步,看看我们想要实现的

query GetFeaturedPlaylists {
featuredPlaylists {
id
name
description
tracks {
id
name
explicit
uri
}
}
}

从这个 中,我们使用 Query.FeaturedPlaylists 函数来解析 featuredPlaylists

Query.cs
public async Task<List<Playlist>> FeaturedPlaylists(SpotifyService spotifyService)
{
var response = await spotifyService.GetFeaturedPlaylistsAsync();
return response.Playlists.Items.Select(item => new Playlist(item)).ToList();
}

当我们将响应转换为 Playlist 对象时,我们使用的是接受 PlaylistSimplified 类型的 Playlist 构造函数。

Playlist.cs
public Playlist(PlaylistSimplified obj)
{
Id = obj.Id;
Name = obj.Name;
Description = obj.Description;
}

在本例中,我们没有对播放列表的曲目进行任何初始化(与接受 SpotifyWeb.Playlist 对象的构造函数相比),这就是我们没有得到任何数据的原因!

请记住,我们需要对 GET /playlists/{playlist_id}/tracks 端点进行额外的调用才能获得曲目列表。我们回到了最初的问题:我们在代码中的哪个位置进行该调用?

我们 可以 在这个构造函数中添加它。这是我们在 函数语句中停下的地方。

Playlist.cs
public Playlist(PlaylistSimplified obj)
{
Id = obj.Id;
Name = obj.Name;
Description = obj.Description;
// should we make a call to spotifyService.GetPlaylistsTracksAsync() here?
}

但这意味着,每当我们 featuredPlaylists 时,我们都会 始终 对 REST API 进行额外的网络调用,即使 没有 请求 tracks

所以,我们将使用 链。

沿着解析器链

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

让我们从我们的项目中举一个例子。这个 GetPlaylist 检索播放列表的名称。

query GetPlaylist($playlistId: ID!) {
playlist(id: $playlistId) {
name
}
}

解析此 时, 将首先调用 Query.Playlist() 函数,然后调用 Playlist.Name() 函数,该函数返回 string 类型并结束链。

Resolver chain in a diagram

此链中的每个 都将它们的返回值传递给下一个函数,使用解析器的 parent

请记住,一个 可以访问多个参数。到目前为止,我们已经使用了 SpotifyService)和 (如播放列表 id )。 parent 是另一个这样的参数!

在本例中, Playlist.Name() 函数可以访问 Query.Playlist() 返回的 Playlist 对象。

让我们看看另一个

query GetPlaylistTracks($playlistId: ID!) {
playlist(id: $playlistId) {
name
tracks {
uri
}
}
}

这一次,我们添加了更多 并请求了每个播放列表的曲目列表,特别是它们的 uri 值。

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

Resolver chain in a diagram

请注意,由于 Playlist.Tracks 返回一个可能包含多个曲目的列表,因此此 可能多次运行以检索每个曲目的 URI。

沿着 的轨迹,Playlist.Tracks() 将能够访问 Playlist 作为 父级Track.Uri() 将能够访问 Track 对象作为 父级

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

重构 Playlist.Tracks

现在我们知道了什么是 链,我们可以用它来确定将播放列表的曲目附加 REST API 调用插入的最佳位置。

记住,我们一直在讨论是否将其包含在构造函数部分,在那里它将被 每次 调用,即使 没有包含它:

Playlist.cs
public Playlist(PlaylistSimplified obj)
{
Id = obj.Id;
Name = obj.Name;
Description = obj.Description;
// should we make a call to spotifyService.GetPlaylistsTracksAsync() here?
// probably not!
}

相反,我们将重构 Playlist.Tracks 属性为一个带有函数体的 函数。现在,它是一个简单的 get; 属性

首先,让我们为 tracks 添加一个私有 ,在它前面加上一个下划线(_)以遵循通用约定。我们将使它可为空。

Playlist.cs
private List<Track>? _tracks;

然后,我们将更新 Playlist(SpotifyWeb.Playlist obj) 构造函数以设置此私有 的值。

Playlist.cs
- Tracks = obj.Tracks.Items.Select(item => new Track(item.Track)).ToList();
+ _tracks = obj.Tracks.Items.Select(item => new Track(item.Track)).ToList();

接下来,我们将把 Tracks 属性转换为一个带有函数体的 函数,而不是 get; set; 方法。

Playlist.cs
- public List<Track> Tracks { get; set; }
+ public List<Track> Tracks()
+ {
+
+ }

现在,我们将返回私有 _tracks 中的内容。

Playlist.cs
return _tracks;

我们的 GetPlaylistDetails 应该仍然可以使用这些更改。花点时间保存我们的更改并确认!

现在我们有一个完美的时机来进行我们的附加 HTTP 调用。

与其立即返回 _tracks 私有 ,我们将检查它是否存在。如果存在,则返回它,但如果 不存在,我们将进行 HTTP 调用。

Playlist.cs
if (_tracks != null) {
return _tracks;
} else {
// TODO: HTTP call
}

由于这是一个常规的 函数,因此我们可以在函数参数中访问 SpotifyService 类。我们还将更新函数使其异步并返回 Task<List<Track>>

Playlist.cs
public async Task<List<Track>> Tracks(SpotifyService spotifyService)

接下来,在 else 块中,让我们调用服务的 GetPlaylistsTracksAsync 方法并 await 结果。

var response = await spotifyService.GetPlaylistsTracksAsync();

GetPlaylistsTracksAsync 方法需要一个 :播放列表的 ID。我们如何获取该值?

嗯,此方法属于 Playlist 类,因此我们可以访问 this.Id

Playlist.cs
var response = await spotifyService.GetPlaylistsTracksAsync(this.Id);

另一种访问播放列表 ID 的方法是通过 parent 参数在 函数中使用 [Parent] 属性。此属性使用依赖注入将 parent 的值注入到 中。

Playlist.cs
public async Task<List<Track>> Tracks(
SpotifyService spotifyService,
[Parent] Playlist parent
)

函数体中,我们将替换 this.Idparent.Id

Playlist.cs
// var response = await spotifyService.GetPlaylistsTracksAsync(this.Id); // same as below
var response = await spotifyService.GetPlaylistsTracksAsync(parent.Id);

两种方法都有效!我们将坚持使用第一种方法,使用 this.Id

让我们完成我们的 函数。在调用 GetPlaylistsTracksAsync 后,我们将深入 responseItems 属性,遍历集合并从每个项目中创建一个 Track 对象。这应该很熟悉,我们已经在 Playlist (SpotifyWeb.Playlist obj) 构造函数中做了同样的事情!不要忘记在最后使用 ToList() 来匹配此 预期返回的类型。

Playlist.cs
return response.Items.Select(item => new Track(item.Track)).ToList();

探索时间:第二轮!

服务器正在运行最新的更改?很好!现在当我们跳回到沙盒并运行 获取 featuredPlaylists 及其曲目列表时,我们将得到我们想要的结果!

query GetFeaturedPlaylists {
featuredPlaylists {
id
name
description
tracks {
id
name
explicit
uri
}
}
}

👏👏👏

与 REST 方法比较

是时候再次戴上我们的产品应用开发人员帽子了!让我们比较一下如果我们使用 REST 而不是 ,这个功能会是什么样子。

如果我们使用 REST,应用程序逻辑将包含

  • /browse/featured-playlists 终结点进行 HTTP GET 调用
  • 对响应中的 每个播放列表 进行额外的 HTTP GET 调用,以获取 GET /playlists/{playlist_id}/tracks。等待所有这些解析,根据播放列表的数量,可能需要一段时间。此外,这引入了常见的 N+1 问题
  • 仅检索 idnameexplicit 以及 uri 属性,丢弃 所有其他 响应。还有很多其他没有使用的响应!同样,如果客户端应用程序的网络速度慢或数据量很少,那么大型响应会带来成本。

使用 ,我们从客户端获得简短、简洁、干净、可读的 ,以他们指定的精确形状返回,不多不少!

提取数据、进行额外的 HTTP 调用以及过滤哪些 是必需的所有逻辑都在 端完成。我们仍然有 N+1 问题,但它是在服务器端(响应和请求速度更加一致,通常更快),而不是在客户端(网络速度 且不一致)。

注意:我们可以使用数据加载器在 端解决 N+1 问题。 查看 Hot Chocolate 文档以了解如何实现它们

主要要点

  • 一个 链是解析特定 时调用解析器函数的顺序。它可以包含顺序路径以及并行分支。
  • 此链中的每个 都将它们的返回值传递给下一个函数,使用解析器的 parent

下一步

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

上一页

分享您关于本课程的问题和评论

本课程目前处于

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

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