使用GraphQL为移动客户端创建统一接口

与Trulia的其他工程团队一样,iOS团队也了解与我们的整体架构相关的挑战。 我们处理了性能问题,发布延迟以及iOS功能与其他平台上的功能之间普遍缺乏对等的问题。 我们经常受到上游决策的负面影响,被迫提出变通办法和权宜之计,只会加重我们面临的许多挑战。 Trulia从单块架构过渡到微服务架构的决定为移动团队提供了一个机会,以永久解决我们的许多挑战,并通过实施GraphQL来偿还我们的一些技术债务。

技术债务:移动API

与整体交互所产生的问题可以追溯到我们的移动应用程序的创建。 这包括我们面临的最大挑战-我们在移动客户端上所需的数据通常分散在多种服务中,仅通过复杂的关系进行连接。 这种低效的配置产生了多个请求,返回了我们不需要的数据,并浪费了资源。

我们通过创建外观REST接口解决了该问题。 外墙处理了处理整体架构的各个部分以及将来自各种来源的数据序列化为一致结构的所有复杂问题,移动客户端可以轻松使用它们。

虽然这简化了我们与整体的交互,但也带来了其他挑战。 移动应用程序需要访问的任何新API也将需要由Facade实现,从而导致更长的总体开发时间。 上游API接口的更改也需要应用于外观,这需要双重维护。 负责各种API和外观的团队的路线图通常不一致,导致Web应用程序和移动应用程序之间存在差异

在考虑转向微服务架构时,我们想了解如何在满足每个客户需求的同时保留外观的优点。

解决方案:GraphQL

我们决定使用基于GraphQL的API创建一个统一的接口,该接口仅以一致的格式向各个客户端提供必要的数据。 单一界面的使用鼓励所有利益相关者参与并为总体架构和领域模型的设计做出贡献,从而确保满足每个团队的需求。

这种合作的例子发生在Trulia Neighborhoods的模式设计的早期。 我们意识到,各个客户端对图像压缩类型的偏好有所不同-iOS支持HEIF压缩,Android首选WebP,而移动Web则首选png。 GraphQL允许我们添加参数,以便每个客户端可以传递其首选的压缩格式以获取远程图像资源。

此外,在GraphQL下进行统一提供了将通常在每个客户端上实现的业务逻辑转移到GraphQL层中的解析器的机会。 当业务规则指示如何显示数据更改时,这消除了客户更新代码的要求。

积分

事实证明,使用Apollo提供的SDK和工具来创建和处理对GraphQL服务器的请求,实施GraphQL是一个简单的过程。 该工具集使我们能够使用GraphQL语法编写查询,并自动生成在每个唯一客户端中执行查询所必需的代码。 虽然我们通过单一查询语言进行统一,但iOS查询是在Swift中生成的,Android查询是在Java / Kotlin中生成的,而Web查询是在TypeScript中生成的。

安卓系统

public final class LocationQuery implements Query { 

private final LocationQuery.Variables variables;

public LocationQuery(@Nonnull Input id) { ... }

@Override
public LocationQuery.Data wrapData(LocationQuery.Data data) { ... }

@Override
public LocationQuery.Variables variables() { ... }

public static final class Builder { ... }

public static final class Variables extends Operation.Variables { ... }

public static class Data implements Operation.Data { ... }

public static class Location {

final @Nonnull String __typename;

final @Nullable Long id;

final @Nullable String name;

...

}
}

的iOS

 public final class GetLocationQuery: GraphQLQuery { 
public let operationDefinition =
"query getSurrounding($id: Int) {\n Location(id: $id) {\n __typename\n id\n name\n }\n}"

public var id: Int?

public init(id: Int? = nil) {
self.id = id
}
...
public struct Data: GraphQLSelectionSet {
...

public var location: Location? { ... }

public struct Location: GraphQLSelectionSet {
...

public var id: Int? { ... }

public var name: String? { ... }
}
}
}

挑战性

尽管轻松集成GraphQL,我们仍然面临一些技术挑战。 首先,我们需要找到一种同时支持GraphQL以支持较新功能以及旧式REST端点的方法。 我们决定采用一种混合方法,该方法利用了通过Apollo客户端处理GraphQL请求这一事实。

Apollo客户端的组件之一是网络传输,它负责序列化和执行网络请求,并将原始响应数据传递到数据解串器。 这个模式已经存在于我们当前的网络堆栈中,该堆栈自动处理了我们应用程序所需的某些特定于域的标头的注入。

幸运的是,Apollo客户端使用面向协议的模式,并允许开发人员注入自己的网络传输。 我们利用现有的网络层,创建了一个符合Apollo的网络传输协议的自定义网络传输,从而消除了在两个网络层之间复制逻辑的必要。

接下来,我们需要找到一种解决方案,以协调GraphQL层创建的通用数据模型与从REST端点接收的数据。 Apollo客户端会根据传入的GraphQL查询自动生成模型。创建的模型被命名为其相应查询的名称空间。 这为两个相同的查询请求相同的数据,具有相同的原始响应,但创建两个完全不同的命名空间模型提供了可能性。 这种情况不太可能发生,但对于我们的混合方法却证明是有问题的。

为了解决此问题,我们决定将Apollo SDK创建的模型视为原始数据。 对于以前不存在的域模型,我们创建了一个新模型来镜像GraphQL模型中的数据。 对于现有的域模型,我们创建了由自动生成的GraphQL模型实现的协议,以将其转换为现有的域模型。 这使我们可以在整个代码库中使用统一的数据表示形式。

向前进

尽管集成GraphQL并非没有挑战,但我们对结果和未来展望感到满意。 我们已大大减少了上游决策对我们的移动应用程序的影响,提高了性能并创建了跨平台的奇偶校验。 将我们的API整合到一个GraphQL层中将减少维护多个API所花费的时间,从而使我们的团队能够专注于构建可帮助消费者找到他们喜欢居住的地方的产品。

您可以在Tech and Innovation博客上了解我们的Web应用程序如何从我们实施GraphQL的决定中受益,以及与我们向微服务架构过渡有关的其他主题。