还在用 Redux,要不要试试 GraphQL 和 Apollo?

还在用 Redux,要不要试试 GraphQL & Apollo?

前段时间刷 Twitter 的时候看到大 V 纷纷提到 Apollo,预测它将在 2018 年崛起。正巧碰上有使用 GraphQL 的机会,在大概翻了下 Apollo 的文档之后,我下定决心在新的前端项目里尝试下抛开已经熟悉的 Redux,完全使用 Apollo 来写数据层。一个月后的现在,我必须出来好好赞美下这位“太阳神”了。

GraphQL

转眼已经 2018 年了,GraphQL 已不再是个新鲜的名词了。15 年短暂的掀起一波讨论之后,似乎也没有听到多少它的声音了。然而 Github 在这几年里慢慢成熟,Github 也将新版 api 完全用 GraphQL 实现。在这里我就不展开讨论 GraphQL 的本身了,它让前后端之间的数据获取变得更加简单。

Redux

提到前端数据管理,最先想到的就是 Redux,我想很多人都体验过对 Redux 从陌生到熟悉的各个阶段,大致应该是这样的:

  • 开始:Facebook 设计的 Flux 架构,很厉害的样子,大家都在用那我也用吧
  • 半年:数据管理变的清晰些,终于不用在组件里来回混乱的 setState 了
  • 一年:我就是个 CRUD 工程师,写个千篇一律的列表,表单页用 redux 真是折腾,多些了多少代码
  • 一年半:看了 redux-action,redux-promise,dva, mirror ...,根据团队的业务场景定制了最合适的中间件和插件代码又变的简洁啦
  • 两年:该折腾的都折腾过了。有点累了,但是也离不开了。

为什么累了呢?因为 Flux 的单向数据流对你来说已经不再新鲜了。大部分时候,store 里存放的都是从后端请求来的数据,对于它们而言,怎么样做 dispatch 和 reduce 其实并不是关键,反倒是怎么设计 store 值得考虑。

当 Redux 遇上业务需求

让我们直接以一个真实的场景作为例子吧:

这是一个很常见的评论列表,拿到需求后我们就开始写我们的 <Comments /> 组件了,在 Redux 的范式下,我们难免要按照这个逻辑来写:

  1. CommentsdidMount 里,dispatch 一个获取数据的 action在这fetch action 内发送请求。为了做 loading,我们很可能要再 dispatch 一个 action 去通知 redux 我们发起了一个请求。
  2. 如果请求顺利成功了,我们 dispatch 一个请求数据成功的 action,然后在 reducer 内处理并更新数据。
  3. Comments 内我们收到了 props 传来的数据,正式开始渲染

我们大量的工作花费在了如何获取数据上。而我们面临的挑战又是什么呢?看几个产品经理们可能会提的需求

  1. 用户创建或修改评论,要能立刻在列表中看到更新;

简单,重新请求一遍整个列表接口就好了!一般而言确实足够了,不过要求高的产品可能会要求你做”乐观“更新来让体验更好。这也没什么问题,加个 reducer 就是。

  1. 当鼠标 hover 在用户头像上的时候,要弹出用户的详细数据(个人简介,联系方式...)

首先你会想,后端大哥能不能把这些字段都帮我加在评论的接口数据里,他毫不犹豫的拒绝了你,拿出一个 commonUser 的接口让你自己去调。细一想用户数据量不小,评论里也有大量的相同用户,不放在列表里也确实合理。心一横,干脆把前端这里的数据结构全部 normalize 化,按用户 id 为 key 用哈希表来存放数据。也就一个下午,你得到了一个非常完美的解决方案。

面对这样的场景,我们写了太多的 命令式 代码,我们一步步的描述了怎么去获取评论数据,在得到评论数据后再提取出所有的用户 id,去重后再次请求获取所有的用户数据,等等。我们还需要考虑缓 normalize,缓存,乐观更新等等细节上的问题。而这些,恰恰是 redux 帮不了我们的。于是我们会基于 Redux 封装更强大的库和框架,但真正 focus 在数据获取上的好像还真没看到非常合适的。

Declarative(声明式) vs Imperative(命令式)

那么在 Apollo 的世界里是什么样的呢?

import { graphql } from 'react-apollo';

const CommentsQuery = gql`
    query Comments() {
        comments {
            id
            content
            creator {
                id
                name
            }
        }
    }
`;

export default graphql(CommentsQuery)(Comments);

我们使用了 graphql(类比到 redux 中的 connect) 作为高阶组件将一条 GraphQL 的查询语句绑定到了 Comments 组件上,然后你所有的一切就准备就绪了。这么简单么?是的,我们不再需要描述怎么在 didMount 里发送请求,怎么处理请求来的数据。而是委托 Apollo 帮我们处理这些所有事情,它会称职的帮我们在需要的时候发送请求获取数据,然后将 data 映射到 Comments 的 props 中交给我们。

不止于此,当我们做更新操作的时候也会便捷许多。比如修改一条评论。我们定义一个 graphql 的 mutation 操作:

// ...

const updateComment = gql`
    mutation UpdateComment($id: Int!,$content: String!) {
      UpdateComment(id: $id,content: $content) {
        id
        content
        gmtModified
      }
    }
`;

class Comments extends React.Component {
    // ...
    onUpdateComment(id,content) {
        this.props.updateComment(id,content);
    }
    
    // ...
}

export default graphql(updateComment)(graphql(CommentsQuery)(Comments));

当我们调用 updateComment 时,你就会神奇的发现,列表中的评论数据自动更新了。这是因为 apollo-client 把数据按照类型自动缓存在了 cache 中,GraphQL 节点返回的任何数据都会自动被用来更新缓存,在 UpdateComment 这个 mutation 中,我们定义了它的返回值,一条类型为 Comment 的新修改评论,并且指定了需要接受的字段,contentgmtModified。这样,apollo-client 就会自动通过 id 和类型去更新缓存中的数据,从而重新渲染我们的列表。

再看看剩下的需求,我们需要在鼠标停留在用户头像时展开用户详情。这个需求下我们不仅仅需要定义我们需要什么数据,还会关心“怎么”获取数据(在 hover 头像时发送请求)。Apollo 同样为我们提供了 “命令式” 的支持

class UserItem extends React.Component {
    // ...
    onHover() {
        const { client,id } = this.props;
        
        client.query({
           query: UserQuery,variables: { id }
        }).then(data => {
            this.setState({ fullUserInfo: data });
        });
    }
}

export default withApollo(UserItem);

幸运的是这里我们依然不需要自己考虑缓存的问题。得益于 Apollo 全局的数据缓存,当我们查询用户 A 之后,再次查询相同 id 的数据会直接命中缓存,apollo-client 会直接 resolve 缓存中的数据,并不发送请求。这时候问题来了,假设我就是想要每次都重新查询呢?

client.query({
   query: UserQuery,variables: { id },fetchPolicy: 'cache-and-network'
});

Apollo 给我们提供了很多策略来自定义缓存逻辑,比如认的 cache-first (优先使用缓存),这里的 cache-and-network(先使用缓存,同时发请求更新),以及 cache-onlynetwork-only

这些就是 GraphQL 和 Apollo 很吸引我的一些地方。当你开始从 GraphQL 的角度来思考,你更多的关心的是你的业务组件需要什么数据,而不是怎么一步步的获得它。而剩下的大部分业务场景,都可以通过前端的数据类型推导和缓存自动解决掉。当然,篇幅有限,还有很多优雅的地方来不及提及,比如分页,直接操作缓存达到乐观更新,轮询查询,以及数据订阅等等。如果有机会的话我们可以继续深入探讨。

REST 和其他本地状态 ?

看到这里,你可能会觉得 “GraphQL 很酷,Apollo 也很酷,但是我的后端是 REST,目前是与他们无缘了”。其实不然,从 Apollo Client 的 2.0 版本开始引入了 Apollo Link,理论上来说我们可以通过 GraphQL 从任何类型的数据源获取数据。

“通过 GraphQL“ 意味着我们可以使用书写 GraphQL 的查询语句来获取无论是 rest api 或是 client state 中的数据,这样 Apollo Client 可以替我们管理应用中所有的数据,包括缓存和数据拼接。

const MIXED_QUERY = gql`
    query UserInfo() {
        // graphql endpoint
        currentUser {
            id
            name
        }
        // client state
        browserInfo @client {
            platform
        }
        // rest api
        messages @rest(route: '/user/messages') @type(type: '[Message]') {
            title
        }
    }
`;

在这一个 Query 查询中,我们使用 GraphQL 的 directive 拼接了来自于 GraphQL,rest,client state 中的数据,将它们抽象在一起维护。与之类似的,我们还可以封装相应的 mutation 实现。

尾巴

以上大概就是我这段时间使用 Apollo 和 GraphQL 的一些浅浅的实践。虽然接触的不深,但我可以感受到 Thinking in GraphQL 为前端带来的更优雅的解决方式,和 Apollo Client 这样一个完整的前端数据层解决方案的高效。我相信在 2018 年,它们会迎来更大的增长,甚至有代替 redux 成为通用数据管理方案的可能。

Apollo 相关的社区也比较活跃,在 dev-blog.apollodata.com 上也经常发表一些很有参考价值的文章,有兴趣可以随便看看~

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom