同学,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。 (由于豆瓣官方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家族来帮忙处理。这个月被借调去支撑另一个团队,学习的步伐好像又要放慢了。哎。。。。。。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。