GraphQL进阶篇: 挥手Redux不是梦
同学,GraphQL了解一下:基础篇
同学,GraphQL了解一下:实践篇
首先,需要澄清,这有点标题党,像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。从数据结构以及流程上来说,基本是这样的:
不论是redux还是apolloClient,其实从大体流程来讲,都是这个思路,只是具体的实现有差别,为了实现简便,用了一个tabBar来代替Search,通过tab的切换来改变status,然后发送请求,更新list,来看一下具体实现:
index.js
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家族来帮忙处理。这个月被借调去支撑另一个团队,学习的步伐好像又要放慢了。哎。。。。。。