4. 编写查询解析器
15m

GraphQL 查询获取数据的方式

我们已设计模式 并配置了我们的 ,但我们的服务器不知道如何 使用 来填充模式 。要解决此问题,我们将定义一组

解析器是一个函数,它负责填充架构中单个字段的数据。每当客户端查询某个特定 时,为该字段从适当 中获取所请求的数据。

一个 函数返回以下某一项:

  • 对应架构 (字符串、整数、对象等)所需的类型的数据
  • 用所需类型的数据兑现承诺

解析器函数签名

在开始编写 之前,我们先来看看解析器函数的签名是什么样的。解析器函数可以选择接受四个位置

fieldName: (parent, args, context, info) => data;
参数说明
父级这是该字段的父级(该父级字段的解析器总是早于该字段子项的解析器执行)解析器的返回值。
参数此对象包含为此字段提供的全部GraphQL 参数
上下文此对象在为特定操作执行的所有解析器中共享。使用此对象可共享基于操作的状态,例如身份验证信息和对数据源的访问。
信息其中包含有关操作的执行状态的信息(仅在高级情况下使用)。

在这 4 个中,我们定义的主要会使用context。它使我们的能够共享我们的LaunchAPIUserAPI 的实例。要想了解它的工作原理,我们开始创建一些

定义顶级解析器

如上所述,父始终在该字段子级的解析器之前执行。因此,首先为一些顶级字段(即Query类型)的字段定义解析器。

正如src/schema.js所示,我们的架构Query类型定义了三个launcheslaunchme。要为这些定义,请打开server/src/resolvers.js并粘贴以下代码:

server/src/resolvers.js
module.exports = {
Query: {
launches: (_, __, { dataSources }) =>
dataSources.launchAPI.getAllLaunches(),
launch: (_, { id }, { dataSources }) =>
dataSources.launchAPI.getLaunchById({ launchId: id }),
me: (_, __, { dataSources }) => dataSources.userAPI.findOrCreateUser(),
},
};

如下代码所示,我们定义了解决器,使用映射,将映射的键与模式的类型(Query)和launcheslaunchme)相对应。

关于上述函数

  • 所有三个函数将第一个位置parent)分配给 _,作为惯例表示它们不使用其值。

  • The launchesme 函数将其第二个位置args)分配给 __,原因相同。

    • launch 函数确实使用 args ,但是,原因在于我们模式的 launch 使用 id 。)
  • 所有三个函数使用第三个位置(context)。具体来说,它们会对其解构,以访问我们定义的dataSources

  • 没有哪个函数包含第四个位置(info),因为它们不使用它,也没有其他需要包含它的原因。

如您所见,这些函数很短!这是因为它们所依赖的大部分逻辑是LaunchAPIUserAPI 的一部分。通过将保持精简作为最佳做法,您可以在安全地重构您的后端逻辑的同时,降低破坏 API 的可能性。

向 Apollo Server 添加解析器

现在我们有了一些,让我们将它们添加到我们的服务器中。将高亮行添加到src/index.js中:

server/src/index.js
const { ApolloServer } = require("apollo-server");
const typeDefs = require("./schema");
const { createStore } = require("./utils");
const resolvers = require("./resolvers");
const LaunchAPI = require("./datasources/launch");
const UserAPI = require("./datasources/user");
const store = createStore();
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
launchAPI: new LaunchAPI(),
userAPI: new UserAPI({ store }),
}),
});
server.listen().then(() => {
console.log(`
Server is running!
Listening on port 4000
Explore at https://studio.apollographql.com/sandbox
`);
});

通过向提供您的映射,它便知道如何按需调用解析器函数以履行传入的查询。

任务!

运行测试查询

让我们在我们的服务器上运行一个测试!使用npm start启动它并返回Apollo Sandbox(我们之前用它来探索我们的模式)。

粘贴以下进入面板:

# We'll cover more about the structure of a query later in the tutorial.
query GetLaunches {
launches {
id
mission {
name
}
}
}

然后,单击运行按钮。服务器的响应显示在右侧。请参阅响应对象如何匹配的结构?这种对应的基本特征。

现在让我们尝试获取GraphQL 参数。粘贴以下并运行它:

query GetLaunchById {
launch(id: "60") {
id
rocket {
id
type
}
}
}

这个返回详细信息Launch对象具有;id 60

而不是硬编码上面,这些工具允许您定义变量对于您。以下使用替代60

query GetLaunchById($id: ID!) {
launch(id: $id) {
id
rocket {
id
type
}
}
}

现在,粘贴以下内容到该工具面板:

QUERY_VARIABLES
{
"id": "60"
}

在继续之前,请随时尝试更多运行查询和设置

定义其他解析器

您可能已经注意到上面运行的测试查询中包含了一些 ,而我们甚至还没有编写 。但是,这些查询仍然成功运行!那是因为 为您未定义自定义 default resolver的任何 定义一个

一个默认的 函数使用以下逻辑:

default resolver logic

对于我们架构的大多数(但不是全部) ,默认 执行的正是我们希望它执行的操作。让我们为需要它的架构 field定义一个自定义 Mission.missionPatch

的定义如下:

type Mission {
# Other field definitions...
missionPatch(size: PatchSize): String
}

Mission.missionPatch编写的 应该根据 size 指定 LARGESMALL返回不同的值。

中添加以下内容,将其映射到 src/resolvers.js,置于 Query属性下方:

server/src/resolvers.js
// Query: {
// ...
// },
Mission: {
// The default size is 'LARGE' if not provided
missionPatch: (mission, { size } = { size: 'LARGE' }) => {
return size === 'SMALL'
? mission.missionPatchSmall
: mission.missionPatchLarge;
},
},

mission(由默认的 返回的用于我们架构中 Launch.mission)中获取大或小补丁。

现在,我们已经了解如何为 之外的类型添加 ,让我们为 LaunchUser类型的 添加一些 。将以下内容添加到 映射(见 Mission之后):

server/src/resolvers.js
// Mission: {
// ...
// },
Launch: {
isBooked: async (launch, _, { dataSources }) =>
dataSources.userAPI.isBookedOnLaunch({ launchId: launch.id }),
},
User: {
trips: async (_, __, { dataSources }) => {
// get ids of launches by user
const launchIds = await dataSources.userAPI.getLaunchIdsByUser();
if (!launchIds.length) return [];
// look up those launches by their ids
return (
dataSources.launchAPI.getLaunchesByIds({
launchIds,
}) || []
);
},
},

您可能会疑惑,在调用类似 getLaunchIdsByUser这样的函数时,服务器如何知道当前用户的身份。当前还不知道!我们将在下一章中解决这个问题。

分页结果

当前,Query.launches会返回一个长长的 Launch对象列表。这通常比客户端一次需要的信息更多,抓取这么多数据会很慢。我们可以通过实施 分页来提升此 的性能。

分页可确保我们的服务器分小块发送数据。对于编号的页面,我们推荐 基于游标的分页,因为它消除了跳过项目或重复显示同一项目的可能性。在 -based pagination 中,使用一个常量指针(或游标)来跟踪从数据集中何处开始获取下一组结果。

让我们设置分页。在src/schema.js,更新Query.launches以匹配以下内容,并添加一个名为LaunchConnection的新类型,如下所示:

server/src/schema.js
type Query {
launches( # replace the current launches query with this one.
"""
The number of results to show. Must be >= 1. Default = 20
"""
pageSize: Int
"""
If you add a cursor here, it will only return results _after_ this cursor
"""
after: String
): LaunchConnection!
launch(id: ID!): Launch
me: User
}
"""
Simple wrapper around our list of launches that contains a cursor to the
last item in the list. Pass this cursor to the launches query to fetch results
after these.
"""
type LaunchConnection { # add this below the Query type as an additional type.
cursor: String!
hasMore: Boolean!
launches: [Launch]!
}

现在,Query.launches接受两个参数(pageSizeafter)并返回一个LaunchConnection对象。LaunchConnection包括:

  • launches的列表(请求的实际数据)
  • 一个cursor表示数据集中当前位置
  • 一个hasMore布尔值表示数据集是否包含launches中包含的内容之外的任何其他项目

打开src/utils.js并签出paginateResults函数。这是来自服务器分页数据的帮助器函数。

现在,让我们更新必要的函数以适应分页。导入paginateResults并替换launches 函数src/resolvers.js中的以下代码:

server/src/resolvers.js
const { paginateResults } = require("./utils");
module.exports = {
Query: {
launches: async (_, { pageSize = 20, after }, { dataSources }) => {
const allLaunches = await dataSources.launchAPI.getAllLaunches();
// we want these in reverse chronological order
allLaunches.reverse();
const launches = paginateResults({
after,
pageSize,
results: allLaunches,
});
return {
launches,
cursor: launches.length ? launches[launches.length - 1].cursor : null,
// if the cursor at the end of the paginated results is the same as the
// last item in _all_ results, then there are no more results after this
hasMore: launches.length
? launches[launches.length - 1].cursor !==
allLaunches[allLaunches.length - 1].cursor
: false,
};
},
},
};

让我们测试我们刚刚实现的分页。使用npm start重新启动服务器,并在中运行以下

query GetLaunches {
launches(pageSize: 3) {
launches {
id
mission {
name
}
}
}
}

得益于我们的分页实现,服务器现在应该仅返回三个而不是完整列表。

任务!

这样就处理好了我们架构查询的 !接下来,我们来为架构的 编写解析器。

上一个