chaussen

状态管理的未来:Apollo的GraphQL语言服务器

原文链接: dev-blog.apollodata.com

在Apollo的开发工具(DevTools)中查询程序当前状态

状态管理的未来

在Apollo客户端程序(Apollo Client)里使用链路状态(apollo-link-state)软件包管理本地数据

一个程序随着大小的增加,状态常常会变得更复杂。作为开发人员,我们的任务不仅是要同时兼顾来自多个远程服务器的数据,还要处理由用户界面互动得来的本地数据。总而言之,我们要把数据的每个部分都以某种方式存储起来,使得程序无论从的哪个组件那里都能轻松地获取数据。

许许多多开发人员告诉过我们Apollo客户端程序(Apollo Client)非常擅于管理远程数据,这差不多占了他们80%的数据需求,但剩下的20%,如全局开关和设备接口结果之类的本地数据怎么办呢?

在过去,Apollo用户靠Redux库或MobX库的独立store对象来管理那20%。这种方法在Apollo客户端程序(Apollo Client)1.0版里也是行得通的,但Apollo客户端程序(Apollo Client)2.0版里没有Redux部分了,要同步两个store对象间的本地和远程数据就变得更困难了。常常听到用户反映,他们想把程序的所有状态都封装进Apollo客户端程序(Apollo Client)里去,保证信息来源的单一性

方案建立在坚实基础上

我们知道这个问题必须解决,所以自问:要在Apollo客户端程序(Apollo Client)里管理状态的话看上去会是怎么样的?首先,我们考虑了Redux库一些讨人喜欢的特色,比如其开发工具,以及通过connect将状态和组件绑定等功能。我们还考虑了Redux带来的痛点,比如异步行为生成器、缓存和优化界面之类的核心功能要用到样板和DIY方法来实现才行。

为了能理想地管理状态,我们想过要以Redux的优点作为基础建立方案,同时解决一些受到批评的问题。我们也想过利用GraphQL语言的优势,只用一个查询(query)语句就能向多个来源请求数据。

Apollo客户端程序(Apollo Client)数据流向架构图

学习一次就能在任何地方使用GraphQL语言

关于GraphQL语言有一种常见的误解,那就是这种模式要与某种特定的服务器实现方法结合在一起。实际上,这种模式要灵活得多。无论是从gRPC服务器上,或是REST端点上,或是客户端缓存里请求获取数据, GraphQL语言都可以用作一种通用数据语言,完全不管数据来源。

这就是为什么用GraphQL语言查询(query)与修改(mutation)来描述程序形成的状态是完美的。我们可以用GraphQL修改(mutation)操作来表示状态的变化,可以用GraphQL查询(query)操作来声明表达组件所需要的数据,以此访问状态数据。

GraphQL的一大优点是能把从多个来源得到的数据汇总,可以是本地的也可以是远程的。在一个查询(query)操作里指定数据域指令即可。?让我们看看怎么做吧!

用Apollo客户端程序(Apollo Client)实现状态管理

在Apollo客户端程序(Apollo Client)里管理本地数据可以通过Apollo Link这个模块化网络栈来实现,它能在任意一点上与GraphQL请求操作的循环挂钩。要从GraphQL服务器上访问数据,我们可以用HttpLink链路,但要从缓存中请求本地数据,我们需要安装一个新的链路:链路状态(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客户端程序(Apollo Client)带链路状态(apollo-link-state)模块的初始化

要创建状态链路,用withClientState函数,把解析器(resolvers)对象默认(defaults)对象和Apollo的缓存(cache)对象组成一个参数对象传入,然后把状态链路与整个链路拼接起来,作为其中一环。状态链路应该放在HttpLink链路前面,这样程序就能先拦截执行本地的查询(query)与修改(mutation),然后再进入网络。

默认对象(Defaults)

默认(defaults)这个对象表示了创建状态链路时要写入缓存的初始状态。虽然不是必需的,但传入默认(defaults)对象给缓存预热一下是很重要的,因为这样不管什么组件查询(query)数据都不会出现错误。默认(defaults)对象的形态应该反映程序里缓存的计划查询方式。

export const defaults = {
  visibilityFilter: 'SHOW_ALL',
  todos: [],
};

默认(Defaults)代表了想写入缓存的初始状态

解析器(Resolvers)

用Apollo客户端程序(Apollo Client)管理状态时,Apollo缓存就成了程序里所有本地和远程数据的唯一来源。如何更新并访问缓存里的数据呢?这时就要用到解析器了。如果大家在服务器端用过graphql-tools工具,那客户端解析器的类型签名和它是一样的:

fieldName: (obj, args, context, info) => result;

不熟悉这个也不用担心。这里要注意的有两点最重要,一是查询(query)或修改(mutation)部分中的变量是作为第二个参数传入的,二是缓存会自动添加到语句环境里去。

export const defaults = { // 和之前一样 }

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;
    },
  }
}

解析器函数可以用来更新和访问缓存里的数据

要把数据写入缓存的根路径,我们调用cache.writeData函数并传入数据。有时我们写入缓存的数据取决于之前缓存里已有的数据,比如上面addTodo的修改(mutation)部分。在那种情况下,可以用cache.readQuery函数先从缓存里读取,再进行写入。如果要把缓存内已有对象的一部分写进去,也可以选择传入一个id号,它与对象在缓存里的键相对应。因为我们用的是InMemoryCache,键值就是__typename:id

链路状态(apollo-link-state)模块支持异步解析函数,这对于实施异步操作的附加效果很有用,比如访问设备API接口。然而,建议大家不要在解析函数中调用REST端点,而是用[apollo-link-rest](https://github.com/apollographql/apollo-link-rest)库,有自己的@rest指令可用。

@client客户指令

从用户界面发起修改(mutation)操作时,Apollo的网络栈需要知道更新的数据是在客户端还是在服务器上。链路状态(apollo-link-state)模块用的是@client客户指令来设定只用于客户端的数据域。然后,链路状态(apollo-link-state)再为这些域调用解析函数。

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客户指令对本地数据进行修改(mutation)

查询(query)操作看起来和修改(mutation)很像。在查询(query)中有什么异步操作要执行的话,Apollo客户端程序(Apollo Client)会帮忙追踪加载与错误状态。对React框架来说,这些状态可以在this.props.data属性里找到,同时那里还可以找到无数的辅助函数,可以用来重新获取数据、分页和轮询。

有一个让人兴奋的特色是可以在一个查询(query)里请求多个数据源的数据!? 在这个例子里,我们要用Apollo缓存里的可视化过滤器(visibilityFilter)从GraphQL服务器那里请求一个用户(user)数据。

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缓存

如果想要看更多的例子和窍门,想将链路状态(apollo-link-state)整合到程序里去,请到我们更新后的文档页

1.0版路线图

链路状态(apollo-link-state)模块已经很稳定了,在现在的程序里已经够用了,不过我们还是想尽快弄好一些新的特色:

  • 客户端模式: 目前,我们还不支持根据客户端模式进行类型验证。这是因为如果在运行阶段要把graphql-js用于构建验证模式的模块都包括进去,那会极大地增加软件包的大小。与之相反,我们希望能把模式构建转移到生成阶段去,并支持模式自我查询(introspection),这样大家就仍然可以利用到GraphQL的绝妙特色。

  • 辅助组件: 我们的目标是要在Apollo程序中尽量实现无缝状态管理。我们想编写一些React组件,使常规操作不那么冗长,比如在后台实现修改(mutation)本身,同时把变量传进去。

如果对这些问题有兴趣,请加入我们的GitHub小组或Apollo Slack的#local-state频道。我们希望有你的参与和帮助,一起塑造下一代状态管理!?