GraphQL客户端实现入门(除了React之外,也想使用GraphQL)
你好,今天是OPENLOGI的AdventCalendar第11天。
上次我写了一篇关于尝试使用GraphQL进行实现的文章,但主要是关于服务器端的讨论。所以这次我打算作为续篇写一下客户端部分。
关于那个不太清楚的Graphql的传闻,关于客户端库应该怎么做呢?我想尽量简单明了地解释一下。
当我打算写样本代码时,意外地成了一篇读物。
那么,Graphql是由Facebook与Relay库一同推出的,相对来说,它更常与React一起使用,但Graphql并不仅仅是为React而存在的。
GraphQL本身是通过GET请求或POST请求向服务器查询,因此即使没有客户端库也可以使用。
所以,这次我想写一下关于在React/Relay的限制下,如何从客户端处理数据。以Apollo Client为例,它是一款可以独立于所使用的UI库而使用的Graphql客户端工具。我会解释一下所谓的客户端端的Graphql库到底是什么。
本次无需过于关注服务器端的实现,但由于整个项目已上传至GitHub,如需查看源代码,请参考该处。
作为示例,我们将以以下GraphQL模式进行介绍。
查询节点是viewer。如果访问用户已登录,则返回该用户。Viewer具有自己的姓名和feeds。feeds是用于显示用户时间线的列表的概念。
type Query {
viewer: User
}
type User {
id: String
name: String
feeds: [Feed]
}
type Feed {
id: String
title: String
body: String
}
如果对于这个模式,(在登录状态下)发送以下查询时
query {
viewer {
name
feeds {
title
}
}
}
这是返回这样的数据的预期。
{
"data": {
"viewer": {
"name": "harada",
"feeds": [
{
"title": "advent calendar 1st day"
},
{
"title": "advent calendar 2nd day"
}
]
}
}
}
尝试使用Ajax进行获取
现在,使用Graphql从服务器获取数据的最简单方法是发送ajax请求。
axios.post('http://localhost:3000/graphql', {
query: 'query{viewer{name\nfeeds{title}}}',
}).then((res) => {
console.log(res.data);
});
这样投送的话就能获取数据。
换句话说,就是服务器端采用REST时(虽然URL和请求体不同),可以完全以相同的方式处理。即使当前服务器端已实现为REST API,也可以逐步转换为GraphQL!
只需将查询{…}和字符串写在另一个文件中定义并导入后使用,这样更清晰、更合理。但归根结底,只是将查询作为字符串发送给GraphQL服务器。
尝试使用Apollo Client
好吧,那么我们来看一下使用Apollo的Graph QL客户端库Apollo Client会发生什么。
const apolloClient = new ApolloClient({
link: new HttpLink({
uri: 'http://localhost:3000/graphql',
}),
// 略
});
apolloClient.query({
query: gql`
query {
viewer {
id
name
feeds {
id
title
}
}
}
`
}).then((res) => {
console.log(res.data)
})
在所提到的AJAX请求中,唯一不同的是出现了gql标签。gql标签会解析查询字符串并将其转换为AST。但是,就查询的发送实现而言,与AJAX请求几乎没有区别。那么,这个库具有什么优点呢?
GraphQL库的主要关注点是缓存和规范化。
所以我们进入正题。为什么要使用Graphql客户端库呢?当然,一方面是为了方便地发送查询,但我认为这些库的主要特点是响应缓存和数据规范化。
例如,当通过SPA切换回到页面时,您是每次都重新获取数据,然后再进行绘制呢?还是使用之前获取的数据来渲染页面呢?
例如,在执行更新操作后,重新绘制页面时,您是重新获取数据呢?还是使用响应中的数据呢?
根据应用程序的特性可能会有所不同,但每次切换屏幕或进行更新处理时重复获取相同的数据是安全但也是浪费的。
对于以前的数据进行缓存并重新利用的情况,需要确保该数据始终保持最新状态以进行管理。
如果在多个组件或画面中显示相同的数据,那么如何更新所有组件/画面的信息呢?一个基本的解决方案是将数据规范化并存储在应用程序的存储中,而不是保留获取到的原始数据形式。
就像Apollo Client一样,通过查询获取的数据不是直接保存,而是经过规范化后在内部保存。(在Relay中也是如此)
假设使用ApolloClient
query {
viewer {
id
name
feeds {
id
title
}
}
}
如果发送这个查询,会收到以下类似的回应。
{
"viewer": {
"id": "1",
"name": "harada",
"feeds": [
{
"id": "1",
"title": "advent calendar 1st day",
"__typename": "Feed"
},
{
"id": "2",
"title": "advent calendar 2nd day",
"__typename": "Feed"
}
],
"__typename": "User"
}
}
而Apollo 在内部以以下方式进行缓存。
{
"User:1": {
"id": "1",
"name": "harada",
"feeds": [
{
"type": "id",
"id": "Feed:1",
},
{
"type": "id",
"id": "Feed:2",
}
],
"__typename": "User"
},
"Feed:1": {
"id": "1",
"title": "advent calendar 1st day",
"__typename": "Feed"
},
"Feed:2": {
"id": "2",
"title": "advent calendar 2nd day",
"__typename": "Feed"
},
"ROOT_QUERY": {
"viewer": {
"type": "id",
"id": "User:1",
}
}
}
可以看出数据是被规范化的,可以更新特定的数据。
在GraphQL中,更新操作被称为Mutation,和查询一样,Mutation也需要定义返回类型。
type Mutation {
updateViewerName(name: String): User
}
当调用updateViewerName这个Mutation时,定义是返回一个User作为响应。当执行这个操作时,
mutation {
updateViewerName(name: "name changed") {
id
name
}
}
使用Apollo Client发送这个Mutation,得到返回的是User类型的对象,因此可以精确更新内部缓存。这个Mutation会将id和name作为User类型的属性投递。
{
"User:1": {
"id": "1",
"name": "harada",
// 省略
"__typename": "User"
},
}
通过这种方式,无论在哪个界面使用User类型的数据,都可以通过一次变更来更新所有内容。不需要进行复杂的实现,比如手动更新响应并存储。
在Redux中,您也可以使用normalizr之类的工具来实现同样的功能。但是在Apollo中,数据的获取、更新以及与组件的协作都在一个库中实现,因此可以相对直观地定义它。
此外,还具备名为Optimistic Update的机制,它允许在发送Mutation后,在收到响应之前,乐观地替换内部数据。由于具备实际数据,因此无需在界面上进行复杂的分支处理,可以相当优雅地实现。
最后
Graphql可以使用简单字符串进行数据获取实现。然而,当使用客户端库时,您会感到无意识地对数据进行管理,不禁让您想这样做,不是吗?
这里要做的是客户端库,例如Relay和Apollo Client。
然而,虽然没有明确提到,但是当进行像将数据添加到数组或从数组中删除数据这样的操作时,这个缓存更新机制会变得有点麻烦。虽然我们拥有规范化的数据,但是从库的角度来看,无法隐式地知道要更改哪个数组。
当应用程序变得复杂时,相同类型的数组可能以多种方式存在,因此在考虑如何反映更新处理结果时,需要仔细设计变异处理,并且维护也相对较为困难。
所以,我已经写了到这里了,但事实上,我个人认为直接使用相对简单的AJAX请求也是一个选择的,,,
我同时抱着淡淡的期望,或许在其中有个美好的未来等待着。