设计可与不同 GraphQL 服务器协同工作的应用程序
「面向接口编程,而非面向实现编程」是一种不直接调用功能,而是通过合约来调用的实践——该合约列举了所需的输入和预期的输出,同时隐藏了具体的实现细节。这一策略有助于将应用程序与特定的实现、提供者或技术栈解耦,使其能够在不修改应用程序代码的情况下进行切换。
我们也可以将这一策略应用于 GraphQL。GraphQL 可以充当应用程序与服务器之间的中间层,使我们能够将所有必要的修改集中在 GraphQL Query 上,而无需改动业务逻辑。
GraphQL Query 作为客户端与服务器之间的接口。执行 Query 时,GraphQL 服务器会对其进行处理并将所需数据返回给客户端。数据从何而来?是如何获取的?客户端不知道,也无需关心。

Query 的响应与 Query 本身具有相同的结构。对于以下 GraphQL Query:
{
post(by: { id: 1 }) {
id
title
}
}...响应将是:
{
"data": {
"post": {
"id": 1,
"title": "Hello world!"
}
}
}对于相同的 Query,若参数不同,返回的数据也会不同,但结构保持不变。这意味着,只要 Query 不变,应用程序就无需更改其读取和处理数据的逻辑,同样也不必关心是哪个 GraphQL 服务器在执行该 Query。
因此,我们可以无缝地将一个 GraphQL 服务器替换为另一个。
Query 依赖于 GraphQL Schema
不过,上一段话略显乐观,因为 GraphQL Query 可能需要根据 GraphQL 服务器进行调整。更准确地说,Query 是基于 GraphQL Schema 的,如果不同的服务器公开不同的 Schema,那么 Query 也会有所不同。
例如,使用 Cursor Connections 规范 的 GraphQL 服务器可能会执行以下 Query:
{
categories(first: 10000) {
edges {
node {
categoryId
description
id
name
slug
}
}
}
}而另一个使用类 WordPress 分页方式的服务器(例如 Gato GraphQL)则会像这样执行相同的 Query:
{
postCategories(pagination: { limit: 10000 }) {
id
description
globalID
name
slug
}
}我们可以看到这两个 Query 之间的差异:
| 特性 | 服务器 #1 | 服务器 #2 |
|---|---|---|
| 文章分类字段 | categories | postCategories |
| 限制结果数量的字段参数 | first | pagination.limit |
对象字段 id 所表示的含义 | 全局唯一 ID | 该类型的唯一 ID |
| Query 结构 | 因 edges.node 而更深 | 更扁平 |
仅将应用程序中第一个服务器的 Query 替换为第二个服务器的等效 Query 是行不通的。因为逻辑仍然会按照原始 Query 的结构和字段来访问响应数据。
一种可能的解决方案是同时替换客户端的数据获取逻辑。例如,以下逻辑:
const categories = data?.data.categories.edges.map(({ node = {} }) => node);...可以替换为:
const categories = data?.data.postCategories;但这恰恰是我们想要避免的。我们希望将变更控制在最小范围内,只修改接口(GraphQL Query),保持业务逻辑不变。
幸运的是,通过仅修改 GraphQL Query,按照以下步骤可以弥合这些差异:
- 将 GraphQL Query 与应用程序解耦
- 通过别名适配字段名
- 通过
self字段适配响应结构
让我们通过这 3 个步骤来了解如何将应用程序适配到不同的 GraphQL 服务器。
将 GraphQL Query 与应用程序解耦
将 GraphQL Query 与应用程序逻辑解耦需要:
- 将每个 GraphQL Query(或一组 Query)存储在单独的文件中,并将所有文件放在特定文件夹中
- 导出这些 Query 并在应用程序中导入使用
例如,我们可以将每个 GraphQL Query 放在 src/data 目录下的单独文件中并导出:
// file `src/data/categories.js`
export const QUERY_ALL_CATEGORIES = gql`
{
categories(first: 10000) {
edges {
node {
databaseId
description
id
name
slug
}
}
}
}
`;应用程序随后可以导入并使用该 GraphQL Query:
import { QUERY_ALL_CATEGORIES } from 'data/categories';
export async function getAllCategories() {
const apolloClient = getApolloClient();
const data = await apolloClient.query({
query: QUERY_ALL_CATEGORIES,
});
const categories = data?.data.categories.edges.map(({ node = {} }) => node);
return {
categories,
};
}得益于这种设置,所有修改只需在 src/data 目录下的文件中进行即可。
通过别名适配字段名
可以使用字段别名将第二个 GraphQL 服务器响应中的字段名重命名为第一个服务器中对应字段的名称。
这样,字段 postCategories、id 和 globalID 就可以使用应用程序期望的名称来获取:分别为 categories、categoryId 和 id:
{
categories: postCategories(pagination: { limit: 10000 }) {
categoryId: id
description
id: globalID
name
slug
}
}请注意,字段 categories 有参数 first,而对应的字段 postCategories 使用参数 pagination.limit。但由于字段参数不会体现在响应中的字段名里,因此无需担心这一点。
通过 self 字段适配响应结构
最后一个挑战稍微复杂一些:我们需要修改响应的结构,为来自 Cursor Connections 规范的 edges 和 node 附加额外的层级。
为此,我们将在 GraphQL Schema 的所有类型中引入一个 self 字段,该字段返回它所应用的同一对象:
type QueryRoot {
self: QueryRoot!
}
type Post {
self: Post!
}
type User {
self: User!
}self 字段允许在不离开被查询对象的情况下向 Query 追加额外层级。执行以下 Query:
{
__typename
self {
__typename
}
post(by: { id: 1 }) {
self {
id
__typename
}
}
user(by: { id: 1 }) {
self {
id
__typename
}
}
}...会得到以下响应:
{
"data": {
"__typename": "QueryRoot",
"self": {
"__typename": "QueryRoot"
},
"post": {
"self": {
"id": 1,
"__typename": "Post"
}
},
"user": {
"self": {
"id": 1,
"__typename": "User"
}
}
}
}现在,我们可以使用 self 人为地附加 nodes 和 edge 层级:
{
categories: self {
edges: postCategories(pagination: { limit: 10000 }) {
node: self {
categoryId: id
description
id: globalID
name
slug
}
}
}
}GraphQL Schema 中 edges 和 self 的对象类型显然不同。但这对应用程序来说并不重要,因为它不与 GraphQL 服务器中建模的实际对象交互。相反,它以 JSON 对象的形式接收数据,无论该字段数据来自 PostConnection 对象还是 Post 对象,内容都是相同的。
请注意,categories 字段通过 self 解析,edges 通过 postCategories 解析,而不是反过来。这是为了确保返回元素的基数与使用 Cursor Connections 规范的字段所定义的保持一致:
type RootQuery {
categories: RootQueryToCategoryConnection
}
type RootQueryToCategoryConnection {
edges: [RootQueryToCategoryConnectionEdge]
}
type RootQueryToCategoryConnectionEdge {
node: Category
}如果适配后的 GraphQL Query 顺序相反(即查询 categories: postCategories 和 edges: self),则数据访问将会失败,因为 data.categories 将是一个数组,执行以下代码时 data.categories.edges 会抛出错误:
const categories = data?.data.categories.edges.map(({ node = {} }) => node);适配所有 Query
对 src/data 中的所有 GraphQL Query 应用相同的策略后,应用程序就能轻松地从一个 GraphQL 服务器切换到另一个。