接上篇 —— Apollo 入门引导(二):连接数据源 —— 继续翻译 Apollo 的官网入门引导。
学习 GraphQL 的查询是如何获取数据的。
Apollo 入门引导 - 目录:
完成时间:15 分钟
前一节已经设计了 schema 并配置了数据源,但是服务不知道如何使用其数据源来填充 schema 字段。为了解决这个问题,接下来将定义一个解析器(resolver)集合。
**解析器的功能是负责为 schema 中的字段填充数据。**每当客户端查询特定字段时,该字段的解析器都会从适当的数据源中获取请求的数据。
解析器函数返回以下之一:
- 解析器对应 schema 字段所需的类型的数据(字符串,整数,对象等)
- 满足所需类型数据的期约(promise)
解析器函数签名
在开始编写解析器之前,先介绍一下解析器函数的签名是什么样的。解析器函数接受四个可选参数:
fieldName: (parent, args, context, info) => data;
| 参数 | 描述 |
|---|---|
parent | 这是该字段的父级的解析器的返回值(父级解析器始终在其子字段的子级解析器之前执行)。 |
args | 该对象包含为此字段提供的所有GraphQL 参数。 |
context | 该对象在执行特定操作的所有解析器之间共享。使用此参数可以共享每个操作的状态,例如身份验证信息和对数据源的访问。 |
info | 其中包含有关操作执行状态的信息(仅在高级情况下使用) |
在这四个参数中,我们定义的解析器将主要使用 context 参数。它使我们的解析器可以共享 LaunchAPI 和 UserAPI 数据源实例。要了解其工作原理,下面就开始创建一些解析器。
定义顶级解析器
如上所述,父字段的解析器始终在其子字段的子解析器之前执行。因此,先从一些顶级字段的解析器开始定义:Query 类型。
正如 src/schema.js 所示,我们 schema 的 Query 类型定义了三个字段:launches、 launch 和 me。要为这些字段定义解析器,请打开 src/resolvers.js 并粘贴以下代码:
module.exports = {
Query: {
launches: (_, __, { dataSources }) =>
dataSources.launchAPI.getAllLaunches(),
launch: (_, { id }, { dataSources }) =>
dataSources.launchAPI.getLaunchById({ launchId: id }),
me: (_, __, { dataSources }) => dataSources.userAPI.findOrCreateUser(),
},
};
如该代码所示,我们在**映射(map)**中定义了解析器,其中映射的键对应于 schema 的类型(Query)和字段(launches、launch、me)。
关于上面的函数参数:
- 所有三个解析器函数均将其第一个参数(
parent)分配给变量_,以表示用不到该值。 - 出于相同的原因,
launches和me函数将其 第二个 参数(args)分配给__。- (
launch函数都用到了args参数,因为 schema 的launch字段带有id参数。)
- (
- 三个解析器函数都用到了第三个参数(
context)。具体来说,将其解构以访问之前定义的dataSources。 - 三个解析器函数都没用到第四个参数(
info),所以也不需要包含它。
如你所见,这些解析器函数都很短!原因是它们所依赖的大多数逻辑是 LaunchAPI 和 UserAPI 数据源定义的。使解析器变短是最佳实践,使你可以安全地重构支持逻辑,同时减少破坏 API 的可能性。
将解析器添加到 Apollo 服务
现在我们有了一些解析器,将它们添加到服务中。将 highlight 行添加到 src / index.js 中:
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const { createStore } = require('./utils');
const resolvers = require('./resolvers'); // highlight-line
const LaunchAPI = require('./datasources/launch');
const UserAPI = require('./datasources/user');
const store = createStore();
const server = new ApolloServer({
typeDefs,
resolvers, // highlight-line
dataSources: () => ({
launchAPI: new LaunchAPI(),
userAPI: new UserAPI({ store }),
}),
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
通过像这样向 Apollo 服务提供解析器映射,它自己就知道如何根据需要调用解析器函数来完成传入的查询。
运行测试查询
下面在服务上运行测试查询!使用 npm start 启动它,并打开先前探索你的 schema章节提供的工具:
- [Apollo Studio]中的 Explorer(studio.apollographql.com/dev)
- 位于
localhost:4000的 GraphQL Playground
将以下查询粘贴到工具的编辑器面板中:
# 在之后的教程中将会学习查询结构的更多信息
query GetLaunches {
launches {
id
mission {
name
}
}
}
然后,单击 Run 按钮。服务的响应出现在右侧。看看响应对象的结构是如何与查询的结构匹配的?这种对应关系是 GraphQL 的基本特点。
现在尝试一个带有 GraphQL 参数 的测试查询。粘贴以下查询并运行:
query GetLaunchById {
launch(id: 60) {
id
rocket {
id
type
}
}
}
这个查询返回 id 为 60 的 Launch 对象的详细信息。
通过这些工具,无需像上面的查询那样对参数进行硬编码,而是可以为操作定义 变量(variable)。下面是使用变量而不是60的相同效果的查询:
query GetLaunchById($id: ID!) {
launch(id: $id) {
id
rocket {
id
type
}
}
}
现在,将以下内容粘贴到工具的 Variables 面板中:
{
"id": 60
}
在继续操作之前,可以随意运行查询和设置变量。
定义其他解析器
你可能已经注意到,上面运行的测试查询包含了几个我们还没有编写解析器的字段。但是这些查询仍然可以成功运行!这是因为 Apollo 服务为没有自定义解析器的字段定义了一个默认解析器。
默认的解析器函数使用以下逻辑:
graph TB;
parent("Does the parent argument have a<br/>property with this resolver's exact name?");
parent--No-->null("Return undefined");
parent--Yes-->function("Is that property's value a function?");
function--No-->return("Return the property's value");
function--Yes-->call("Call the function and<br/>return its return value");
对于 schema 的大多数(但不是全部)字段,默认解析器完全可以实现想要的功能。接下来为 Mission.missionPatch 的 schema 字段定义一个自定义解析器。
该字段具有以下定义:
# 不需要复制这段代码
type Mission {
# 其他字段的定义...
missionPatch(size: PatchSize): String
}
Mission.missionPatch 的解析器应返回不同的值,具体取决于查询的 size 参数指定的是 LARGE 还是 SMALL。
在 Query 属性下方的 src/resolvers.js 中,将以下内容添加到解析器映射中:
// Query: {
// ...
// },
Mission: {
// 默认的size为 'LARGE'
missionPatch: (mission, { size } = { size: 'LARGE' }) => {
return size === 'SMALL'
? mission.missionPatchSmall
: mission.missionPatchLarge;
},
},
这个解析器从 mission 获取一个大的或小的徽章,这是 schema 的 父 字段Launch.mission默认解析器返回的对象。
现在,我们知道了如何为 Query 之外的类型添加解析器,继续为 Launch 和 User 类型的字段添加解析器。在Mission 下方将以下内容添加到解析器映射中:
// Mission: {
// ...
// },
Launch: {
isBooked: async (launch, _, { dataSources }) =>
dataSources.userAPI.isBookedOnLaunch({ launchId: launch.id }),
},
User: {
trips: async (_, __, { dataSources }) => {
// 通过user获得发射的id数组
const launchIds = await dataSources.userAPI.getLaunchIdsByUser();
if (!launchIds.length) return [];
// 通过id数组来查询launch数组
return (
dataSources.launchAPI.getLaunchesByIds({
launchIds,
}) || []
);
},
},
你可能想知道服务在调用 getLaunchIDsByUser 之类的函数时如何知道当前用户的身份。目前还不能知道!将在下一章中解决该问题。
分页结果
当前,Query.launches 返回一长串 Launch 对象。这通常比客户端一次需要的信息冗余太多,并且获取大量数据可能速度会很慢。可以通过实现分页来改善该字段的性能。
分页可确保服务分小块发送数据。建议对带编号的页面进行基于游标的分页(cursor-based pagination),因为它消除了跳过一条或多次显示同一条的可能性。在基于游标的分页中,常量指针(或游标)用于在获取下一组结果时,跟踪数据集的起始位置。
下面来设置基于游标的分页。在 src/schema.js 中,更新 Query.launches 以匹配以下内容,并添加一个名为LaunchConnection 的新类型,如下所示:
type Query {
launches( # 替换当前的launches查询
"""
要展示的结果数量,必须>= 1. Default = 20
"""
pageSize: Int
"""
如果在这里添加了一个游标,只返回游标_之后_的内容
"""
after: String
): LaunchConnection!
launch(id: ID!): Launch
me: User
}
"""
launches列表周围的简单包装,其中包含到列表中的最后一项的游标。
将此游标传到launches查询,以在这些之后获取结果。
"""
type LaunchConnection { # 作为一个可选类型,添加在Query类型下面
cursor: String!
hasMore: Boolean!
launches: [Launch]!
}
现在,Query.launches 接受两个参数(pageSize 和 after)并返回一个 LaunchConnection 对象。LaunchConnection 包括:
launches列表(查询请求到的实际数据)cursor,“游标”指示数据集中当前位置hasMore,布尔值,指示数据集是否除launches中包含的项之外还有更多项
打开 src/utils.js 并查看 paginateResults 函数。这是用于从服务分页数据的辅助函数。
现在,来更新解析器函数以适应分页。导入 paginateResults 并用以下代码替换 src/resolvers.js 中的 launches 解析器函数:
const { paginateResults } = require('./utils');
module.exports = {
Query: {
launches: async (_, { pageSize = 20, after }, { dataSources }) => {
const allLaunches = await dataSources.launchAPI.getAllLaunches();
// 希望按时间倒序排列
allLaunches.reverse();
const launches = paginateResults({
after,
pageSize,
results: allLaunches,
});
return {
launches,
cursor: launches.length ? launches[launches.length - 1].cursor : null,
// 如果分页结果末尾的游标与_所有_结果中的最后一项相同,则再之后就没有结果
hasMore: launches.length
? launches[launches.length - 1].cursor !==
allLaunches[allLaunches.length - 1].cursor
: false,
};
},
launch: (_, { id }, { dataSources }) =>
dataSources.launchAPI.getLaunchById({ launchId: id }),
me: async (_, __, { dataSources }) =>
dataSources.userAPI.findOrCreateUser(),
},
};
测试一下刚刚实现的基于游标的分页。使用 npm start 重新启动服务,并在 Explorer / GraphQL Playground 中运行以下查询:
query GetLaunches {
launches(pageSize: 3) {
launches {
id
mission {
name
}
}
}
}
由于我们实现了分页,服务应该只返回三个发射而不是完整列表。
这样就完成了 schema 查询的解析器!接下来,为它的变更编写解析器。
前端记事本,不定期更新,欢迎关注!