[译] 状态管理的将来:在 Apollo Client 中使用 apollo-link-state 管理本地数据

当一个应用的规模逐渐扩张,其所包含的应用状态通常也会变得更加复杂。做为开发者,咱们可能既要协调从多个远端服务器发送来的数据,也要管理好涉及 UI 交互的本地数据。咱们须要以一种合适的方法存储这些数据,让应用中的组件能够简洁地获取这些数据。html

许多开发者告诉过咱们,使用 Apollo Client 能够很好地管理远端数据,这部分数据通常会占到总数据量的 80% 左右。那么剩下的 20% 的本地数据(例如全局标志、设备 API 返回的结果等)应该怎样处理呢?前端

过去,Apollo 的用户一般会使用一个单独的 Redux/Mobx store 来管理这部分本地的数据。在 Apollo Client 1.0 时期,这是一个可行的方案。但当 Apollo Client 进入 2.0 版本,再也不依赖于 Redux,如何去同步本地和远端的数据,变得比原来更加棘手。咱们收到了许多用户的反馈,但愿能有一种方案,能够将完整的应用状态封装在 Apollo Client 中,从而实现单一的数据源 (single source of truth)android

解决问题的基础

咱们知道这个问题须要解决,如今让咱们思考一下,如何正确地在 Apollo Client 中管理状态?首先,让咱们回顾一下咱们喜欢 Redux 的地方,好比它的开发工具,以及将组件与应用状态绑定的 connect 函数。咱们同时还要考虑使用 Redux 的痛点,例如繁琐的样板代码,又好比在使用 Redux 的过程当中,有许多核心的需求,包括异步的 action creator,或者是状态缓存的实现,再或者是积极界面策略的采用,每每都须要咱们亲自去实现。ios

要实现一个理想的状态管理方案,咱们应当对 Redux 取长弃短。此外,GraphQL 有能力将对多个数据源的请求集成在单次查询中,在此咱们将充分利用这个特性。git

以上是 Apollo Client 的数据流架构图。github

GraphQL:一旦学会,随处可用

关于 GraphQL 有一个常见的误区:GraphQL 的实施依赖于服务器端某种特定的实现。事实上,GraphQL 具备很强的灵活性。GraphQL 并不在意请求是要发送给一个 gRPC 服务器,或是 REST 端点,又或是客户端缓存。GraphQL 是一门针对数据的通用语言,与数据的来源毫无关联。后端

而这也就是为什么 GraphQL 中的 query 与 mutation 能够完美地描述应用状态的情况。咱们可使用 GraphQL mutation 来表述应用状态的变化过程,而不是去发送某个 action。在查询应用状态时,GraphQL query 也能以一种声明式的方式描述出组件所须要的数据。缓存

GraphQL 最大的一个优点在于,当给 GraphQL 语句中的字段加上合适的 GraphQL 指令后,单条 query 就能够从多个数据源中获取数据,不管本地仍是远端。让咱们来看看具体的方法。bash

Apollo Client 中的状态管理

Apollo Link 是 Apollo 的模块化网络栈,能够用于在某个 GraphQL 请求的生命周期的任意阶段插入钩子代码。Apollo Link 使得在 Apollo Client 中管理本地的数据成为可能,从一个 GraphQL 服务器中获取数据,可使用 HttpLink,而从 Apollo 的缓存中请求数据,则须要使用一个新的 link: apollo-link-state服务器

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink } from 'apollo-link';
import { withClientState } from 'apollo-link-state';
import { HttpLink } from 'apollo-link-http';

import { defaults, resolvers } from './resolvers/todos';

const cache = new InMemoryCache();

const stateLink = withClientState({ resolvers, cache, defaults });

const client = new ApolloClient({
  cache,
  link: ApolloLink.from([stateLink, new HttpLink()]),
});
复制代码

以上代码是使用 apollo-link-state 初始化 Apollo Client。

要初始化一个 state link,需要将一个包含 resolversdefaultscache 字段的 object 做为参数,调用 Apollo Link 中的 withClientState 函数。而后将这个 state link 加入 Apollo Client 的 link 链中。该 state link 应该放在 HttpLink 以前,这样本地的 query 和 mutation 会在发向服务器前被拦截。

Defaults

前文的 defaults 字段是一个用于表示状态初始值的 object,当 state link 刚建立时,这个默认值会被写入 Apollo Client 的缓存。尽管不是必需的参数,不过预热缓存是一个很重要的步骤,传入的 default 使得组件不会由于查询不到数据而出错。

export const defaults = {
  visibilityFilter: 'SHOW_ALL',
  todos: [],
};
复制代码

以上代码的 defaults 表明了 Apollo cache 的初始值。

Resolvers

在使用 Apollo Client 管理应用状态后,Apollo cache 成为了应用的单一数据源,包括了本地和远端的数据。那么咱们应当如何查询和更新缓存中的数据呢?这即是 Resolver 发挥做用的地方了。若是你之前在服务器端使用过 graphql-tools,那么你会发现二者的 resolver 的类型签名是同样的。

fieldName: (obj, args, context, info) => result;
复制代码

若是你没见过以上这段类型签名,没关系张,只需记住重要的两点:query 或者 mutation 的变量经过 args 参数传递给 resolver;Apollo cache 会做为 context 参数的一部分传递给 resolver。

export const defaults = { // same as before }

export const resolvers = {
  Mutation: {
    visibilityFilter: (_, { filter }, { cache }) => {
      cache.writeData({ data: { visibilityFilter: filter } });
      return null;
    },
    addTodo: (_, { text }, { cache }) => {
      const query = gql`
        query GetTodos {
          todos @client {
            id
            text
            completed
          }
        }
      `;
      const previous = cache.readQuery({ query });
      const newTodo = {
        id: nextTodoId++,
        text,
        completed: false,
        __typename: 'TodoItem',
      };
      const data = {
        todos: previous.todos.concat([newTodo]),
      };
      cache.writeData({ data });
      return newTodo;
    },
  }
}
复制代码

以上的 Resolver 函数是查询和更新 Apollo cache 的方法。

若要在 Apollo cache 的根上写入数据,能够调用 cache.writeData 方法并传入相应的数据。有时候咱们须要写入的数据依赖于 Apollo cache 中原有的数据,例如上面的 addTodo 方法。在这种状况下,能够在写入以前先用 cache.readQuery 查询一遍数据。若要给一个已经存在的 object 写一个 fragment,能够传入一个可选参数 id,这个参数是相应 object 的 cache 索引。上文咱们使用了 InMemoryCache,所以索引的形式应当是 __typename:id

apollo-link-state 支持异步的 resolver 方法,能够用于执行一些异步的反作用过程,好比访问一些设备的 API。然而,咱们不建议在 resolver 中对 REST 端点发请求。正确的方法是使用 [apollo-link-rest](https://github.com/apollographql/apollo-link-rest),这个包里包含有 @rest 指令。

@client 指令

当应用的 UI 触发了一个 mutation 以后,Apollo 的网络栈须要知道要更新的数据存在于客户端仍是服务器端。apollo-link-state 使用 @client 指令来标记只需存在于客户端本地的字段,而后,apollo-link-state 会在这些字段上调用相应的 resolver 方法。

const SET_VISIBILITY = gql`
  mutation SetFilter($filter: String!) {
    visibilityFilter(filter: $filter) @client
  }
`;

const setVisibilityFilter = graphql(SET_VISIBILITY, {
  props: ({ mutate, ownProps }) => ({
    onClick: () => mutate({ variables: { filter: ownProps.filter } }),
  }),
});
复制代码

以上这段代码经过 @client 指令将数据修改限制在本地。

Query 的形式和 mutation 相似。若是在 query 中使用了异步的查询,Apollo Client 会为你追踪数据加载和出错的状态。若是使用的是 React,能够在组件的 this.props.data 中找到相应的数据,里面还会有不少辅助方法,例如重发请求、分页以及轮询等功能。

GraphQL 的一个很让人激动的功能是在单个 query 中向多个数据源请求数据。在下面的例子中,咱们在同一条 query 内查询了 GraphQL 服务器中存储的 user 数据以及 Apollo cache 中的 visibilityFilter 数据。

const GET_USERS_ACTIVE_TODOS = gql`
  {
    visibilityFilter @client
    user(id: 1) {
      name
      address
    }
  }
`;

const withActiveState = graphql(GET_USERS_ACTIVE_TODOS, {
  props: ({ ownProps, data }) => ({
    active: ownProps.filter === data.visibilityFilter,
    data,
  }),
});
复制代码

以上代码使用 @client 指令查询 Apollo cache。

在咱们 最新的文档页中,能够找到更多的例子,以及一些将 apollo-link-state 集成在应用中的小贴士。

1.0 版本前的路线图

尽管 apollo-link-state 的开发已足够稳定,能够投入实际应用的开发了,但仍有一些特性咱们但愿能尽快实现:

  • 客户端数据模式:当前,咱们还不支持对客户端数据模式结构的类型校验,这是由于,若是要将用于运行时构建和校验数据模式的 graphql-js 模块放入依赖中,会显著增大网站资源文件的大小。为了不这点,咱们但愿能将数据模式的构建转移到项目的构建阶段,从而达到对类型校验的支持,并也能够用到 GraphiQL 中的各类很酷的功能。
  • 辅助组件:咱们的目标是让 Apollo 的状态管理尽量地与应用无缝链接。咱们会写一些 React 组件,使得某些常见需求的实现再也不繁琐,譬如在代码层面上容许直接将程序中的变量做为参数传递给某个 mutation 当中,而后在内部直接以 mutation 的方式实现。

若是你对上述问题感兴趣,能够在 GitHub 上加入咱们的开发和讨论,或者进入 Apollo Slack 的 #local-state 频道。欢迎你来和咱们一块儿构建下一代的状态管理方法!


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索