资讯专栏INFORMATION COLUMN

GraphQL and Relay 浅析

Luosunce / 826人阅读

摘要:包括什么把关于数据获取的事情都接管过来,比如说请求异常,,请求排队,,获取分页数据。的声明式数据获取是按组织的,最好的方式也是把需要的数据写在。另外,通过声明式数据获取还可以更好的对组件约束,只能获取它声明的数据,并且也可以做些验证。

Facebook 在去年夏天公布了 GraphQL,就像往前端深潭砸下了一颗巨石,人们都被水声吸引到了湖边,观望是否会出现什么,有些人期待,有些人猜疑。过了半年多,社区已经慢慢的摸清这个石头的材质,本文希望在你入门 GraphQL 和 Relay 的过程中能帮你清除一些障碍。

GraphQL

GraphQL 是在 Facebook 内部应用多年的一套数据查询语言和 runtime。
初次入门者建议先把官网的资料都读一遍,难度不大(specification 和 API 可以后面再看)。

GraphQL 包括什么

类型系统 - GraphQL 是强类型语言,强类型虽然写时会稍微累点,但就不用写一堆类型检测的代码了;

验证 - GraphQL 提供机制对你的语法和请求做一定层度的校验;

introspection - 一个让你能通过几行代码就能了解整个资源提供方的细节的 API。

GraphQL 优势

官网已经列举了,我用更简练的语言描述下。

GraphQL 与 REST

同类型协议目前最出名的是 REST,特点是资源可定位,使用 HTTP verbs。REST 具体应该怎么写有很多争议,但简单的例子是没有争议的:

GET /users/1

REST 优点是简单明了,缺点也是太简单明了,导致语法可扩充性不强。
我们来看看 GraphQL 官网是怎么和 REST 对比的:

语法灵活

GraphQL 只需要一次请求就能够获得你所有想要的资源。这里举一个和 REST 对比的例子 让大家有直观的认识。

现在,我想获取id为1的用户的名字,年龄和他所有朋友的名字

GraphQL 实现的方案:

{
  user(id: 1) {
    name
    age
    friends {
      name
    }
  }
}

REST 实现的方案:

GET /users/1 and GET /users/1/friends  

GET /users/1?include=friends.name

发现区别了吗?用 REST 要不就发多次请求,要不就得用一个不方便扩展的语法。

没有冗余

日后扩充资源也没有冗余,你只会获得你想要的资源。还是用上面的例子,如果 user 多了个属性 gender 会怎么样?
在 REST 的方案中,如果客户端不变,取到的结果是会多了 gender 属性,而在 GraphQL 方案中,客户端是不会获取到 gender 属性的。

强类型

有 introspection 机制,代码即文档,方便快捷,而不需要去找这个 API 的说明文档在哪里,看个例子:

自定义 schema

没必要像 REST 这样固定且通用的语法。

其他专有方案(Ad Hoc Endpoints)

和专有方案对比:

专有方案每个接口都自己定义获取数据,后端代码不能得到重用;

和 REST 对比的第二点一样;

每个接口的数据不能复用;

对比其他现有的专有方案,要么没有强类型,要么没有 GraphQL 这么昂贵,而且前面3点也还是没有解决。

与图数据库的关系

首先,介绍下什么是图数据库,可以参考neo4j的介绍,一图胜千言:


上边是关系数据库,下边是图数据库。

GraphQL 为什么有 Graph,是因为它的 query 是以图的形式来组织的:

user
┖-OWNS-> playlist
         ┖-CONTAINS-> track
                      ┖-LIKED_BY-> users

GraphQL 并不要求后台一定要是图数据库,关系数据库也可以,它只是一套查询数据的语言而已。

DataLoader

Dataloader 是一个小工具,帮你把你的请求转成批量请求的形式,和 GraphQL 搭配的也挺好,看个例子:

query FetchPlaylist {
  playlist(id: "e66637db-13f9-4056-abef-f731f8b1a3c7") {
    id
    name

    tracks {
      id
      title

      viewerHasLiked
    }
  }
}

这个 query 是要获取某个用户的歌单。
注意一个细节,这个 query 想获取每个 track 的一些属性。我们定义一下 Track 这个类型:

import {
  GraphQLString,
  GraphQLBoolean,
  GraphQLObjectType
} from "graphql";

export default new GraphQLObjectType({
  name: "Track",
  description: "A Track",
  fields: () => ({
    id: {
      type: GraphQLString,
      resolve: it => it.uuid
    }

    title: { type: GraphQLString },

    viewerHasLiked: {
      type: GraphQLBoolean,
      resolve: (it, _, { rootValue: { ctx: { auth } } }) => (
        (auth.isAuthenticated) ? it.userHasLiked(auth.user) : null
      )
    }
  })
});

resolve 函数调用的是后端 API,注意这里的 it 就是 track 的对象。
我们获取 viewerHasLiked 这个属性需要调用 it.userHasLiked (auth.user)。那么,我的歌单里有 50 首歌的话,就要调用 50 次it.userHasLiked(auth.user),这样访问数据库的性能是无法接受的。合理的想法是变成批量的。那要怎么做呢?这就是 DataLoader 发挥作用的时候了:

import DataLoader from "dataloader";
import BaseModel from "./BaseModel";

const likeLoader = new DataLoader((requests) => {
  // requests is now a an array of [track, user] pairs.
  // Batch-load the results for those requests, reorder them to match
  // the order of requests and return.
})

export default class Track extends BaseModel {
  userHasLiked(user) {
    return likeLoader.load([this, user]);
  }
}

在一个 event loop 里每次调用 dataloader,dataloader 会记下你的请求参数,在下次 event loop 的时候把这么多次的请求参数变成一个数组提供你操作,你就可以拿这个数组对数据库执行批量的操作了。而且,它还对结果按你的请求参数进行了缓存,是居家必备的杀人利器。

安全性

或许有人有疑问,感觉 GraphQL 把我所拥有的资源全部都暴露了,别人不只一览全局,而且还能一次过全部拉下来,那还得了?
事实上,GraphQL 提供的资源不一定要和你数据库一样,因为它只是扮演中间层的角色,虽然也可能很像。所以,你要想好哪些资源可以被看。
至于获取,其实看到上面的例子里有这句 auth.isAuthenticated

可以看到你可以在里面插入权限限制的。至于获取资源太多拖垮服务器?

Jacob Gillespie 提到一些思路:

对语句做 AST 分析,太复杂的就拒绝了;

做超时限制,对容量也可以做限制;

客户端记得要做 cache(如 Relay)。

Relay

Relay 是连接 GraphQL 和 React 的一座桥梁。不过,除了让 React 认识 GraphQL 服务器之外,它还做了什么呢?

建议先把官网的资料都读一遍,Relay 相对来说比 GraphQL 复杂一些,而且文档并不详细(截至截稿时,Relay的版本是 v0.6.1),也缺失了关于 graphql-relay 库的详细介绍,扫一遍后,结合本文最后的学习资料的代码加深理解。

Relay 怎么用?

使用 Relay 是要侵入前后端的:

在后端你得通过 graphql-relay-js 让 GraphQL schema 更适合 Relay;

在前端再通过 react-relay 来配合 React。

Relay 包括什么?

Relay 把关于数据获取的事情都接管过来,比如说请求异常,loading,请求排队,cache,获取分页数据。我这里重点讲一下以下几个方面:

client-side cache

Relay 获取数据当然离不开 cache,可以看到 GraphQL 不再依赖 URL cache,而是按照 Graph 来 cache,最大的保证 cache 没有冗余,发最少的请求,我举一个例子:

比如下面这个请求:

query { stories { id, text } }

如果利用 URL 请求(比如说浏览器的 cache),那么这个请求下次确实命中 cache 了,那么假如我还有一个请求是:

query { story(id: "123") { id, text } }

看得出,下面这个请求获取的数据是上面请求的子集,这里有两个问题:

如果第一第二两个请求获取的数据不一致怎么办?

本来就是子集,为什么我还要发请求?

这两个想法催生出来了 GraphQL 的解决方案:按照 Graph 来 cache,也就是说子集不需要再发请求了,当然你也可以强制发请求来更新局部或者整个 cache。

具体做法是通过拍平数据结构(类似数据库的几个范式)来 cache 整个 Graph。

view 通过订阅他需要的每个 cache record 来更新,只要其中一个 record 更新了,也只有订阅了这个 record 的 view 才会得到更新。

最后,聊到修改,我们可以看到 mutation 有个反直觉的地方是请求的 query 里包括了需要获取的数据。为什么不直接返回你的修改影响的那些数据? 因为服务端实现这个太复杂了,有的时候一个简单的修改会影响到非常多的后台数据,而很多数据 view 是不需要知道它变化了。

所以,Relay 团队最后选择的方案是,让客户端告诉服务器端你认为哪些数据你想重新获取。具体到实现,Relay 采用的方案是获取 cache 和 fat query 有交集的部分,这样既更新了 cache,而且不在 cache 里的也不会获取。

Relay 的声明式数据获取

React 是按 Component 组织 view 的,最好的方式也是把 view 需要的数据写在 view。如果用常规的做法,view 负责自己的 Data-fetch,那么,由于 React 是一层一层的往里深入 Component 的,那么也就意味着每一层 Component 都自己发请求去了,是不可能做到用一个网络请求来获取所有数据的。

所以,Relay 通过抽象出一个 container 的概念,让每个模块提前声明自己需要的数据,Relay 会先遍历所有 container,组成 query tree,这样就达到了只使用一个网络请求的目的。

另外,通过声明式数据获取还可以更好的对组件约束,只能获取它声明的数据,并且 Relay 也可以做些验证。

graphql-relay-js

在看一些 React 和 Relay 协作的例子时,经常发现这个库的存在,这个库到底是干什么的?

通过查看源码后发现,里面其实是各种 helper 方法,负责生成一些 GraphQL 的类,为什么需要这样做?其实,这是因为 Relay 提供的一些功能(比如 ID handling,分页)需要 GraphQL 服务器提供特定的代码结构。如果你要开发一个 GraphQL 的前端,就算它基于其他框架,基于其他语言,实现一个像 graphql-relay-js 所实现的 Relay-compliant 的 server 是很有帮助的,比如graphql-go/relay。

babel-relay-plugin

Relay 的 container 依赖的数据资源是通过声明的,但客户端是不知道后端的数据结构的。为了让客户端了解整个后台结构,就要引入这个 bable 插件,这个插件通过读取服务端的 schema,就可以让客户端正确理解它所需要的资源在服务端是长什么样的。

optimistic UI update

我们看下例子:

Loading...
; }} renderFailure={function(error, retry) { return (

{error.message}

); }} />

可以看到在 Relay 里可以很简单的处理请求整个请求过程中的 UI 变化。

总结

相信阅读本文的读者都是对这两者有一定兴趣的人,但在我上手之后,我的心情是复杂的。GraphQL 和 Relay 带来了一些优势,最重要的是可以一次性获取资源,看上去是未来之路,但这优势其实用些不优雅的方法来解决也没什么问题,但为了这些优势需要编写大量与业务逻辑无关的代码,让我真心忧虑它的路能走多远,相信看过一个官方的 TODOList的例子 的入门者很容易就能感觉到。REST 如此简单,普及开来尚且用了几年,复杂好多倍的 GraphQL 的未来还任重而道远。

学习资料

GraphQL 和 Relay学习资源汇总:这里列举了比较全的相关学习资源,5颗星。

搭建你的第一个 GraphQL 服务器:这篇文章从0开始帮你搭建一个 GraphQL,比较浅,3颗星。

relay-starter-kit:这个例子简单的描述了 Relay 和 GraphQL 的关系,但没有 mutation,3颗星。

From rest to GraphQL:提到了rootValue,dataloader,讲了比较真实的例子,5颗星。

Relay 官方例子 TODOlist:比较完整的增删改查的官方例子,5颗星。

Unofficial Relay FAQ:这篇 FAQ 是 Facebook 员工写的,里面提到 Relay 是要取代 Flux,而且 routing 还在积极修改中。

相关的库

server:比如 express-graphql。

ORM:比如 graffiti。

facebook/dataloader。

adrenaline:React bindings for Redux with Relay。

react-router-relay:结合 react-router,介绍。

graphql-relay-js

babel-relay-plugin

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78869.html

相关文章

  • Flux再进化:Introducing Relay and GraphQL

    摘要:它的设计使得即使是大型团队也能以高度隔离的方式应对功能变更。获取数据数据变更性能,都是让人头痛的问题。通过维护组件与数据间的依赖在依赖的数据就绪前组件不会被渲染为开发者提供更加可预测的开发环境。这杜绝了隐式的数据依赖导致的潜在。 关于Relay与GraphQL的介绍 原文:Introducing Relay and GraphQL 视频地址(强烈建议观看):https://www.y...

    cncoder 评论0 收藏0
  • 简单暴力!21 分钟学会 apollo-client + redux

    摘要:阅读过程中如果产生任何不适,请及时拨打自行抢救,谢谢。端选型总体还是比较前后端分离的,不强制你使用某一种方案。是官方出品和推荐的,也是默认的配套方案。事后来看,的坑不少。 apollo-client 是一个比较难用的 GraphQL 客户端,本系列带你集成 redux,趟平深坑,钻入原理,让你在 21 分钟内学完 apollo-client。 NOTE: 阅读过程中如果产生任何不适,请...

    rockswang 评论0 收藏0
  • Graphql实践——像axios一样使用Graphql

    摘要:初始化项目使用初始化项目安装项目结构如下接口所有接口对封装接下来对进行封装,加上中间件实现类似于拦截器的效果。 Graphql尝鲜 在只学习graphql client端知识的过程中,我们常常需要一个graphql ide来提示graphql语法,以及实现graphql的server端来进行练手。graphql社区提供了graphiql让我们使用 graphiql (npm):一个交互...

    mumumu 评论0 收藏0
  • 翻译 | React AJAX最佳实践

    摘要:作者沪江前端开发工程师本文原创翻译,有不当的地方欢迎指出。管理数据,而提供服务器上的数据,因此应用于处理网络请求。结论使用建立的应用都是模块化的会成为其中一个模块,库是另一个模块。原文原创新书移动前端高效开发实战已在亚马逊京东当当开售。 作者:Oral (沪江Web前端开发工程师)本文原创翻译,有不当的地方欢迎指出。转载请指明出处。 当你问起有关AJAX与React时,老司机们首先就会...

    DirtyMind 评论0 收藏0
  • react如何和server交互

    摘要:在一个应用中,如何通过和端进行交互这个问题曾经困扰了我一段时间,经过学习实践,有了一点心得体会,写出来和大家分享一下。组件和一样,和进行交互,将获取的通过向下传递给组件。不足被设计用来和服务器一起运行,并不能很好的和第三方服务交互。 在一个react应用中,如何通过ajax和server端进行交互这个问题曾经困扰了我一段时间,经过学习实践,有了一点心得体会,写出来和大家分享一下。 总的...

    1treeS 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<