记在 React 项目中使用 GraphQL
概述
最近在项目中对接了GraphQL,遇到了一些困难和疑惑,记录下来供后续参考。
GraphQL是一种的查询语言;作为前端开发,我们可以通过编写GraphQL query自定义返回数据的结构,来更加方便/贴合视图层面的消费。
例子:
GraphQL:
{
hero {
name
height
mass
}
}
数据返回:
{
"hero": {
"name": "Luke Skywalker",
"height": 1.72,
"mass": 77
}
}
关于对GraphQL的定义不再赘述,更多可以在GraphQL网站上查阅。
然而GraphQL网站只是对关于这个查询语言的描述/规范,并没有相关的实现。也就是说,如果我们需要在项目中落地,需要另找针对JavaScript的实现(资源包)。
GraphQL简单来说包含两面:后端实现和客户端实现。由于我方后端比较给力,已经做好了后端实现,我们需要做的只需要做客户端的事情。
在 GraphQL网站上列举了很多 JavaScript 的实现,我们选择了最热门的 Apollo Client。
开始
安装依赖
假定已经使用Create React App的生成了基础的项目模板以后,在主目录运行:
yarn add @apollo/client graphql
# 或者
npm install @apollo/client graphql -S
其中:
@apollo/client
apollo 客户端,包含了缓存/状态管理/错误处理(react hooks api),和视图层面(React组件)等 API;graphql
包含了基础 GraphQL 查询语句分析功能。这个包我们通常不会直接使用到,只是 @apollo/client 的依赖 。
初始化client实例
新建文件 src/lib/graphql.ts
存放client实例,方便我们后续在应用中导入:
import { ApolloClient, InMemoryCache } from '@apollo/client'
let graphUrl = '//somethingcool.com/graphql'
// 区分开发/测试环境与生产环境,此处的 process.env.mode 为 webpackDefinePlugin 自定义内容;实际情况自行按需编写
if (process.env.NODE_ENV === 'development' || process.env.mode === 'dev') {
graphUrl = '//somethingcool-test.com/graphql'
}
export const client = new ApolloClient({
uri: graphUrl,
cache: new InMemoryCache(),
credentials: 'include'
})
注意,有时候开发主域和接口的域名不一定相同,由于不符合浏览器同源策略,这就会出现跨域问题。在服务端配置支持 CORS 的前提下,在服务端需要获得cookie的话,需要配置 credentials: 'include'
。
cache 字段普通使用 new InMemoryCache()
即可,apolloCilent 会缓存我们已经请求过的数据,下次再发出同样的请求的话,apollo 会返回对应条目的记录,而不再发出已有的数据请求。
创建 src/lib/gql.ts
文件, 用来统一存放 query,方便后续统一管理:
import { gql } from '@apollo/client'
// 查询关注列表
export const FollowingListQuery = gql`
query ($uids: [ID!]!) {
getLoginUser {
uid
filterOutAttentionUids(uids: $uids)
}
}
`
// 关注
export const Follow = gql`
mutation ($uid: ID!) {
subscribe(uid: $uid) {
code
errorMessage
success
}
}
`
// 取消关注
export const UnFollow = gql`
mutation ($uid: ID!) {
unsubscribe(uid: $uid) {
code
errorMessage
success
}
}
`
可以看出,query 是没有代码上的依赖,只是描述了query 和 mutation 所需要的信息。
最后一步准备工作是让把 React 视图层和 apollo 客户端连接起来。Apollo Client 提供了一个 ApolloClientProvider
, 我们把它放到组件树较高的地方,这样我们在业务组件调用如 useQuery, useMutaion 的时候,apollo 可以知道我们在操作哪个 client 。
在 src/entry/index.tsx
:
//...
import { ApolloProvider } from '@apollo/client'
import { client } from 'src/lib/graphql'
const App = (
<ApolloProvider client={client}>
<Router>
{/** ... */}
</Router>
</ApolloProvider>
)
ReactDom.render(App, document.getElementById('app'))
在业务中使用
准备工作已经完成,开始做业务开发,这次要实现一个关注列表。因为需要用到的接口不完全可以用 useQuery
完成,所以套用了 ahooks
的 usePagination
组合了下:
import { client } from 'src/lib/graphql'
import { usePagination } from 'ahooks'
import { FollowingListQuery } from 'src/lib/gql'
export const Home = () => {
const { mutate, refresh, data, pagination, loading, error } = usePagination(
({ current, pageSize }) => fetchers['/anchor-list']({
...extra,
pageNo: current - 1,
pageSize
})
.then((res) => {
const uids = res.data
if (uids?.length) {
// 请求关注数据
return client
.query({ query: FollowingListQuery, variables: { uids } })
.then(({ data }) => {
const filterOutAttentionUids = data?.getLoginUser?.filterOutAttentionUids || [] // 已关注用户列表
const anchorList = res.data?.anchorList
if (anchorList && anchorList.length) {
for (const anchor of anchorList) {
anchor.isSubscribed = filterOutAttentionUids.includes(`${anchor.uid}`) // 如果在关注列表,标记
}
}
return res
})
} else {
return res
}
})
.then((res) => ({
list: res.data?.anchorList || [],
total: res.data?.total || 0
}))
)
return // ...
}
关注这段:
client
.query({ query: FollowingListQuery, variables: { uids } })
.then(res => {})
我们先前在 FollowingListQuery 指定的 $uids
可以如此在 variables 字段传入。client.query
返回一个promise 所以我们很方便把数据处理逻辑串联起来。
插播个提示:usePagination 返回的 mutate 帮助函数十分便利。在这个例子中,点击了关注以后,需要在本地更新 isSubscribed ****标识,但是不需要重新发起请求的时候,我们就使用了 mutate 临时改了本地数据。
接下来,我们实现,关注/取消关注按钮组件:
import { useMutation } from '@apollo/client'
import { Follow, UnFollow } from 'src/lib/gql'
// ...
export const SubscriptionBtnSwitcher: FC<Props> = ({ userInfo }) => {
const [subscribe, { loading: subscribing }] = useMutation(Follow)
const [unSubscribe, { loading: unSubscribing }] = useMutation(UnFollow)
return (
<ListActionContext.Consumer>
{({ mutate }) =>
userInfo.isSubscribed ? (
<Button
type='weak'
loading={unSubscribing}
onClick={async () => {
const { data } = await unSubscribe({ variables: { uid: userInfo.uid } })
// mutate local value
if (data.unsubscribe.code === 0) {
mutate((data) => {})
} else {
Toast.info({ text: '取消关注失败' })
}
}}
>
取消关注
</Button>
) : (
<Button
type='emphasis'
loading={subscribing}
onClick={async () => {
const { data } = await subscribe({ variables: { uid: userInfo.uid } })
// mutate local value
if (data.subscribe.code === 0) {
mutate((data) => {})
} else {
Toast.info({ text: '关注失败' })
}
}}
>
关注
</Button>
)
}
</ListActionContext.Consumer>
)
}
}
可以看出,useMutation
的api设计和 React 视图层结合得十分合适:
const [subscribe, { loading: subscribing }] = useMutation(Follow)
useMutation 返回第一个参数我们可以用来给业务调用。在这个例子中,点击关注按钮之后调用
await subscribe({ variables: { uid: userInfo.uid } })
同 client.query 一致,我们通过 variables 传入参数。除此之外,useMutation还返回了第二个参数:{ loading: subscribing }
我们可以在操作结束之前在视图展示加载状态。
总结
经过这么多次的迭代,GraphQL生态已经足够完善,再加上 React 本身的升级,譬如 React Hooks 等特性等结合起来,前端开发体验会变得越来健壮与高效。
参考: