TrailerQL 是一个 Swift 包,它简化了查询 GraphQL 端点的许多步骤。
目前用于
请参阅 TrailerQLTests.swift
以获取此处使用的完整代码
让我们看看瑞克和莫蒂现在在哪里...
let url = URL(string: "https://rickandmortyapi.com/graphql")!
让我们构建我们的查询模式。根据该站点的文档,我们想要创建这个 GraphQL 查询
characters(filter: { name: "Rick" }) {
results {
id
name
status
location {
id
name
type
}
}
}
在 TrailerQL 中,我们使用 Group
、Field
、Fragment
和 BatchGroup
构建 GraphQL 对象关系。我们首先声明顶层组,并将其命名为 characters
。我们还提供一个元组(或更多,如果需要),其中包含参数名称和值。在本例中,我们将整个 { name: "Rick" }
部分作为 filter
的参数值。
let schema = Group("characters", ("filter", "{ name: \"Rick\" }")) {
Group("results") {
Field.id
Field("name")
Field("status")
Group("location") {
Field.id
Field("name")
Field("type")
}
}
}
我们创建一个 Query,并将该模式指定为根元素。在本例中,我们还想禁用 GitHub 风格的速率检查,因为此 API 服务器不支持它。Query 初始化器有很多选项,因此请务必查看更详细的信息。
初始化器在 perNode
中接受一个闭包(或方法),当 TrailerQL 解析 API 响应数据时,会为解析的每个项目调用一次。我们将在下面看到如何做到这一点。
let query = Query(name: "Rick And Morty", rootElement: schema, checkRate: false, perNode: scanNode)
对它的每次调用都接受一个 Node
类型的参数。Node
包含已解析 GraphQL 对象的信息,例如其类型、其 ID 以及其父级的 ID(如果存在)。
TrailerQL 将不会解析不包含 ID 的节点,但它会很乐意“解包”层以查找其中的项目。例如,上面的“characters”组不是一个对象,而是一个容器,而“results”是包含 id 的对象列表。
private func scanNode(_ output: ParseOutput) {
switch output {
case .queryComplete:
print("All nodes from the query received")
case .queryPageComplete:
print("All nodes from returned page of the query are received")
case let .node(scannedNode):
switch scannedNode.elementType {
case "Character":
if let newCharacter = Character(from: scannedNode) {
Character.all.append(newCharacter)
} else {
print("Could not parse character from: \(scannedNode.jsonPayload)")
}
case "Location":
if let newLocation = Location(from: scannedNode) {
if let parentId = scannedNode.parent?.id,
let character = Character.find(id: parentId) {
character.location = newLocation
}
} else {
print("Could not parse location from: \(scannedNode.jsonPayload)")
}
default:
print("Unknown type: \(scannedNode.elementType)")
}
}
}
我们检查 scannedNode.elementType
以了解此回调是关于哪种类型的,然后使用它实例化每个对象。在内部,这些初始化器使用 Node
的 .jsonPayload
属性来访问节点的 JSON 并从中反序列化一个实例。
queryComplete
和 queryPageComplete
可以提示任何处理这些节点的解析逻辑,这些逻辑可能需要知道何时首先传递了查询(或页面)的整个树。
每个 Character
在此端点的模式中都有一个关联的 Location
。因此,既然我们已经创建了一个 Location
,我们检查扫描的 Node
的 .parent
属性,看看是否可以找到 Character
实例并将其添加到那里。
这个 Query
现在为我们生成了我们想要创建的 GraphQL 查询文本,在 query.queryText
中
let queryText = query.queryText
XCTAssert(queryText == " { characters(filter: { name: \"Rick\" }) { __typename results { __typename id name status location { __typename id name type } } } }")
让我们将此转换为 JSON 以发送到 API 端点,方法是将查询文本编码为键为“query”的 JSON 对象,并将其转换为字节。(任何 JSON 编码器都可以,在本例中我们使用的是默认的 Apple 框架)。
let dataToSend = try JSONSerialization.data(withJSONObject: ["query": queryText])
将其发布到 API,确保 API 知道这是 JSON
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = dataToSend
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let result = try await URLSession.shared.data(for: request).0
让我们将响应解析为 JSON 对象...
let resultJson = try JSONSerialization.jsonObject(with: result)
...并将其馈送到 TrailerQL 中。processResponse
方法将运行并为它遇到的每个节点调用我们上面创建的回调。
_ = try await query.processResponse(from: resultJson)
在大型查询中,结果可能表明需要更多分页。如果发生这种情况,该方法将返回一个 Query
对象序列,可以运行这些对象来获取更多节点。
值得注意的是,运行这些查询然后可以返回更多 Query
对象,依此类推,只要有更多数据可用。
在这种情况下,TrailerQL 将尝试创建尽可能少的查询,但保持在 API 端点的最大节点限制之下。
让我们列出我们解析的任何 Character
对象!
for character in Character.all {
print(character.id, character.description, separator: "\t")
}
您可以将 GraphQL 片段与 Fragment
一起使用。例如,上面的查询可以写成
fragment locationFragment on Location {
id
name
type
}
fragment characterFragment on Character {
id
name
status
}
{
characters(filter: { name: "Rick" }) {
results {
... characterFragment
location {
... locationFragment
}
}
}
}
在 TrailerQL 中,这将表示为这样
let characterFragment = Fragment(on: "Character") {
Field.id
Field("name")
Field("status")
}
let locationFragment = Fragment(on: "Location") {
Field.id
Field("name")
Field("type")
}
let schema = Group("characters", ("filter", "{ name: \"Rick\" }")) {
Group("results") {
characterFragment
Group("location") {
locationFragment
}
}
}
在这个例子中,这实际上更复杂且不太有用,但对于我们需要在多个地方查询相同类型的情况,片段既提高了查询速度,也使模式更容易保持统一和可读。想象一下,如果有更多组期望 Character
或 Location
,这将使事情变得多么好。
有时我们不想查询单个模式,而是想查询一堆相同类型的项目。BatchGroup
允许我们通过在一系列 Id 上 spread 一个片段来做到这一点。
假设我们要查询一堆角色的已知 ID。
fragment characterFragment on Character {
id
name
status
location {
id
name
type
}
}
{
charactersByIds(ids: [1,2,3,4,5]) {
... characterFragment
}
}
在 TrailerQL 上,上面将像这样生成...
let queries = Query.batching("Rick And Morty", groupName: "charactersByIds", idList: ["1", "2", "3", "4", "5"], checkRate: false, perNode: scanNode) {
Fragment(on: "Character") {
Field.id
Field("name")
Field("status")
Group("location") {
Field.id
Field("name")
Field("type")
}
}
}
...并像以前一样运行 Query。在这种情况下,根据 ID 列表的大小,可能会有多个查询,因为 TrailerQL 将尝试评估每个批次的节点成本,并使每个批次都低于节点限制(并记住每个查询可能都需要更多分页,具体取决于情况)
版权所有 (c) 2023 Paul Tsochantaris。根据 MIT 许可证获得许可,有关详细信息,请参阅 LICENSE。