GraphQL 进阶篇:挥手 Redux 不是梦
首先,需要澄清,这有点标题党,像 Redux, Mobx,Flux 这种状态管理库,在日常的开发中的地位还是难以撼动的,但是我们可以试着去了解 ApolloClent,它在做本地状态管理所应用的思想,ApolloClient 官方有一片文章: The future of state management 。如果对 GraphQL 还不是很了解的同学,可以看一下开头的两篇文章。作为自己今年下半年学习的重点,如果仅仅去了解好像有点半途而废的感觉,所以我选择如果学,请深钻的道路。
文章所引用的 源码地址
在 实践篇 的最后,我在最后一段抛出 graphql 怎么与现在的 redux 做集成,而 MagicPig 同学在评论里告诉我 ApolloClent 其实可以不依赖第三方库,自己做状态管理。当时自己入门不深,也是一脸懵逼,后面受其指点,在 ApolloClent 官网 转悠,发现还有很多宝藏可以挖掘。
用 ApolloClent 代替 Redux
在 Redux 的官方教程中,曾用一个 TodoList 来介绍 Redux 的状态管理,看下图:
这上面的演示,如果你不是一个 react 新手,应该不会太陌生。在 react 应用中,加入 redux,实现本地添加 list 条目与条目状态切换,以及列表的过滤条件切换,如果关于它的实现还不是很了解,可以到 Redux 官网 重新温习一次。
ApolloClent 的 Local state management 章节,为了说明怎样用 ApoloClient 管理应用的本地状态(Learn how to store your local data in Apollo Client),官方提供了一个示例,应用其 state 功能以及 grapql 本地查询语法,实现了一个拥有同样功能的 TodoList, CodeSandBox 源码地址 ,不过官方提供的这个在线演示,好像是少了些东西,我并没有完全跑成功,我把东西 down 下来,改把,改把,在本地还是跑成功了,想了解的,可以通过上方的地址下载。
基础知识梳理
在实践篇中创建一个 client 实例代码是这样的:
import { ApolloProvider } from 'react-apollo'; import ApolloClient from "apollo-boost"; const client = new ApolloClient({ uri: 'http://localhost:8080/graphql', // 服务端接口 batchInterval: 10, opts: { credentials: 'cross-origin', // App 端单独跑了一个服务,所以涉及到跨域; }, });
上面的代码,就是建立了一个远程的 Graphql 操作服务,而在这里,我们需要加入本地的状态管理,代码变成了这样:
import { ApolloProvider } from 'react-apollo'; import { ApolloClient } from 'apollo-client'; import { withClientState } from 'apollo-link-state'; import { HttpLink } from 'apollo-link-http'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { resolvers, typeDefs, defaults } from '../client/index'; const cache = new InMemoryCache(); const client = new ApolloClient({ cache, // 本地数据存储 link: withClientState({ resolvers, defaults, cache, typeDefs }).concat( new HttpLink({ uri: 'http://localhost:4001/graphql', batchInterval: 10, opts: { credentials: 'cross-origin', }, }) ), });
首先,ApolloClient 这个对象引入的 NPM 包变了,以前是从 apollo-boost 引入的,现在是从 apollo-client 引入的。其次这里加入的本地状态管理,是用 withClientState 创建了一个 link 对象,传入了四个参数(resolvers, defaults, cache, typeDefs),cachce 很简单,就是上面 new InMemoryCache()创建的本地存储,这里简单说明一下 resolvers, defaults, typeDefs。
基本定义
首先需要知道的,ApolloClient 所建立的状态管理思想与 Redux 的操作思路基本一致。只是实现上。ApolloClient 的本地状态管理,是用 Graphql 那一套来做的,即 query, root, resolver, schema 这些概念,建立一套本地的 Query(query, mutation, subscrition)。
- Defaults: 这个和我们写 Redux 一样,通常需要定义一个 initialState, 所以 defaults 是一个为你应用定义的一个初始化对象,这个对象将会被写入 cache,在做客户端查询时,定义一个完整的初始化对象,其有助于减少很多错误,比如,你没有定义,但是去操作它,通常会报一个,you can't read the propery 'xxx' of undefined;
- Typedefs: typeDefs 其实是一个定义本地查询的 Schema, 只不过其加入了更多的语法糖,不用像我们在实践篇用原生 graphql 语言写出的那样冗长, 但其确实就是一个 Scehma,定义了查询对象与各做操作;
- resolvers:这个其实就是描述所有在 Schema 提到的 resolver,总共三类:Query, Mutation, Subscrition,其语法也和服务端的语法一致,每个 resolver 是一个函数,其包括了四个传参(root,args, context, info);
其次,由于 ApolloClient 所建立的本地状态管理,其实建立的是一个本地 graphql 服务,所以不管是对本地还是远程,我们都是用 graphql 语言来进行描述,所以,区分本地与远程就显得十分重要。前者在新建查询时多了一个** @client **参数。 比如:
const query = gql`
query GetTodos {
todos @client {
id
text
completed
}
}
`;
更新本地状态
ApolloClient 提供了两种方式来更新本地状态:Direct writes 与 resolver。Direct writes 就是 new 出来的这个 cache 对象,其包含了一些方法,可以直接对 state 的数据进行操作,它没有采用 graphql 的突变语法来进行数据操作,所以不会执行数据类型的校验,这种方式只适用于一些简单的状态更新,如果这个状态对你的应用很重要,那就应该用更安全的 resolver 方式来代替。
resolver 在前面已经提到,它是 Mutation 的处理方法,会告诉 graphql 怎样更新数据。在后面我们做数据状态更新时,其实也有两种实现方式,一种是实践篇用到的那样,用 graphql 创建一个带变更操作的高阶组件(在实践篇用到的那样),另一种是直接用 react-apollo 提供的 Mutation 组件,示例:
const TOGGLE_TODO = gql` mutation ToggleTodo($id: Int!) { toggleTodo(id: $id) @client } `; const Todo = ({ id, completed, text }) => ( <Mutation mutation={TOGGLE_TODO} variables={{ id }}> {toggleTodo => ( <li onClick={toggleTodo} style={{ textDecoration: completed ? 'line-through' : 'none', }} > {text} </li> )} </Mutation> );
状态查询
状态的查询与读取,是一个最基本的需求,查询语法与服务端语法一致。但不同的是,除了在加载页面的时候需要查询状态,在变更状态时,有时也需要先查询某些关联的状态,然后再做其他操作,比如下面这样:
toggleTodo: (_, variables, { cache }) => { const id = `TodoItem:${variables.id}`; const fragment = gql` fragment completeTodo on TodoItem { completed } `; const todo = cache.readFragment({ fragment, id }); const data = { ...todo, completed: !todo.completed }; cache.writeData({ id, data }); return null; }
上面这一段代码,是关于 todoList 中的每条 List 的状态切换,单击条目将其状态从代办变为已办,或从已办变为代办。代码的实现中有一段为 cache.readFragment,它的目的就是从 cache 中的 TodoItem 属性中获取某个特定 id 条目的状态,然后取反重新写入。除了 cache.readFragment,还有像 cache.readQuery 这样的方法,因为这是本地的状态管理,所以这个是一个同步的操作,就不涉及 promise 的概念。更多关于 cache 方法的操作可查看 官网文档 。
写一个本地与远程的状态管理应用
接下来,将会与我们的日常实践更加接近,就是用 apolloClient 代替现有的 redux,结合 antd 做一个中后台最常见的列表查询页面。一个典型的列表查询页,基本由两部分组成,一个 Search 查询表单头,一个查询结果展示的 table。 (由于豆瓣官方 api 权限的调整,查询读书列表需提供 API KEY)
不论是 redux 还是 apolloClient,其实从大体流程来讲,思路差不多,只是具体的实现有差别,为了实现简便,用了一个 tabBar 来代替 Search,通过 tab 的切换来改变 status,然后发送请求,更新 list,来看一下具体实现:
import TabBar from './TabBar';
import Content from './ContentHoc';
const GET_STATUS = gql`
{
readStatus @client
}
`;
// 每次页面渲染前,从 cache 中读取 status 的值
const BookList = () => (
<Query query={GET_STATUS}>
{({ data: { readStatus } }) => {
return (
<div>
<TabBar status={readStatus} />
<Content status={readStatus} />
</div>
);
}}
</Query>
);
每次页面渲染前,从 cache 中读取 status 的值,然后将其作为 props 传递到 TabBar 与 Content 组件。
/** TabBar.js **/ import { Mutation } from 'react-apollo'; import { Tabs } from 'antd'; import gql from 'graphql-tag'; const TabPane = Tabs.TabPane; const ReadStatus = [{ label: '总书单', value: '', }, { label: '已读', value: 'read', }, { label: '期望读', value: 'wish', }, { label: '正在读', value: 'reading', }]; const ChangeStatus = gql` mutation ChangeStatus($status: String){ changeStatus(status: $status) @client } `; export default class TabBar extends Component { constructor(props) { super(props); this.state = {}; } render() { const { status } = this.props; return ( <Mutation mutation={ChangeStatus} > {changeStatus => ( <Tabs defaultActiveKey={status} onChange={(value) => { changeStatus({ variables: { status: value } }); }}> {ReadStatus.map(({ label, value }) => <TabPane tab={label} key={value} />)} </Tabs> )} </Mutation> ); } }
tabBar 组件根据拿到的 status,渲染 tab 的选中状态,同时给 Tabs 增加了相应的点击事件,来触发 cache 中 readStatus 值的变更。
/** ContentHoc.js **/ import { Query } from 'react-apollo'; import { Table } from 'antd'; import gql from 'graphql-tag'; const columns = [{ title: '序号', dataIndex: 'book_id', key: 'id', }, { title: '书名', dataIndex: 'title', key: 'title', }, { title: 'url', dataIndex: 'image', key: 'image', }]; export const BOOKS_QUERY = gql` query($status: String){ collections(status: $status) { total collections { book_id title image } } } `; export default class BookList extends Component { constructor(props) { super(props); this.state = {}; } render() { const { status } = this.props; return ( <Query query={BOOKS_QUERY} variables={{ status }}> {({ loading, error, data }) => { if (loading) { return <div className="loading">Loading...</div>; } if (error) { return <div className="loading error">error</div>; } const { collections: lists, total } = data.collections; const tableProps = { dataSource: lists, columns, rowKey: 'book_id', }; return ( <div> <p className="total">总共有<span>{total}</span>本图书</p> <Table {...tableProps} /> </div> ); }} </Query> ); } }
这一部分应该是与我们使用 Redux 区别最大的部分,传统的 Redux 用法会将 list 的获取与保存放置在容器组件中,然后通过 props 传递到展示组件。而在这里,利用了 apolloClient 提供的 Query 组件,来做以前容器组件干的活。
然后以前我们需要在请求的过程中捕获错误或请求状态,而在这里,Query 组件提供了一系列的属性(loading,error),可以直接使用,无需自身维护。
另外,为了调试方便,apolloClient 还提供了像 React-developer-Tool 一样的调试工具(需要梯子): Apollo Client Devtools
使用总结
通过一个实践,自我感觉其实不管使用 Redux 还是 apolloClient,我们都采用了相同的思路,只是具体的实现方式有差别,或则说 Redux 与 apolloClient 用两种不同的手段达到了同一种效果:Redux 的 dispatch Type 与 apolloClient 的 query @client 查询。另外,就上面这种简单纯粹的中后台系统,使用 apolloClient 就已足够,不需要再加入 Redux 家族来帮忙处理。这个月被借调去支撑另一个团队,学习的步伐好像又要放慢了。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论