GraphQL 在 client 中的组织方式

GraphQL 在 client 中的组织方式

预研

预研:客户端中使用apollo-client

通过前端工程化将 Apollo 引入现有 React 技术栈

Apollo 优点:

  • 文档丰富
  • 社区驱动,生态丰富
  • 代码侵入性低(相比 Relay)

Apollo 缺点:

  • 非官方出品

Relay 优点:

  • Facebook 官方出品约定大于配置

Relay 缺点:

  • 文档复杂
  • 侵入性较强
  • 手动处理缓存

在react中使用apollo-client

官方文档:https://www.apollographql.com/docs/react/get-started/

官方教程:https://odyssey.apollographql.com/lift-off-part1/

引入react框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ApolloClient, InMemoryCache } from '@apollo/client';
// 初始化 ApolloClient
const client = new ApolloClient({
cache: new InMemoryCache(),
uri: 'http://localhost:4000/graphql',
});

//将ApolloClient实例化对象传入ApolloProvider组件
ReactDOM.render(
<ApolloProvider client={client}>
<Pages />
</ApolloProvider>,
document.getElementById('root')
);

query(查询)

query后端代码: https://codesandbox.io/s/queries-example-app-server-71z1g?file=/index.js

graphql查询语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { gql, useQuery } from '@apollo/client'
// 创建一个查询
const GET_DOGS = gql`
query GetDogs {
dogs {
id
breed
}
}
`
const GET_DOG_PHOTO = gql`
query dog($breed: String!) {
dog(breed: $breed) {
id
displayImage
}
}
`

示例通过playground得到以下数据结构参考

image-20210410204410900

查询hook: useQuery

useQueryhook.返回查询的对象,它包含loading,error,data三种状态,每次查询它都会自动缓存在本地

下面例子包含1. 查询状态(loading) 2. 轮询同步(pollInterval) 3. 手动同步(refetch) 4. 网络状态(networkStatus)的使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 显示狗狗图片组件 gql承接GET_DOG_PHOTO
function DogPhoto({ breed }) {
// loading为 查询中的状态:Boolean
// refetch为 可以向服务端手动刷新query: Function
// networkStatus (1 = loading,2 = setVariables,3 = fetchMore,4 = refetch,6 = poll,7 = ready,8 = error): Number
const { loading, error, data, refetch, networkStatus } = useQuery(GET_DOG_PHOTO, {
// 传递gql所用的参数
variables: { breed },
// 监听同步的状态,为true时可以看到networkStatus
notifyOnNetworkStatusChange: true,
// 轮询与服务器数据同步:单位毫米秒
pollInterval: 500
})

// 同步时
if (networkStatus === 4) return <p>Refetching!</p>
// 第一次加载时
if (loading) return <p>loading...</p>
if (error) return `Error!: ${error}`

return (
<div>
<div>
<img src={data.dog.displayImage} style={{ height: 100, width: 100 }} />
</div>
{/* refetch为手动同步,pollInterval为自动同步*/}
<button onClick={() => refetch()}>Refetch!</button>
</div>
)
}

懒查询: useLazyQuery被调用时,它不会立即执行其相关的查询,会返回一个可执行函数

下面例子,点击按钮它才会执行query

1
2
3
4
5
6
...
const [getDog, { loading, data }] = useLazyQuery(GET_DOG_PHOTO)
...
<button onClick={() => getDog({ variables: { breed: 'bulldog' } })}>
Click me!
</button>

Mutation(更新)

mutation后端代码: https://codesandbox.io/s/queries-example-app-server-71z1g?file=/index.js

graphql查询语法

面下例子,第一个gql为增加,第二个gql为更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { gql, useMutation } from '@apollo/client'
// "增加",(type: $type) 传参
const ADD_TODO = gql`
mutation AddTodo($type: String!) {
addTodo(type: $type) {
id
type
}
}
`
// "更新"单个,传入唯一值id和内容
const UPDATE_TODO = gql`
mutation UpdateTodo($id: String!, $type: String!) {
updateTodo(id: $id, type: $type) {
id
type
}
}
`

更新钩子: useMutation

以下为useMutation简单的使用方法

1
2
3
4
5
6
7
8
...
// 返回的数组第一项为mutate可执行函数
// 第二项为数据状态,有loading态和error态,和useQuery一样
const [addTodo, {loading: mutationLoading, error: mutationError , data }] = useMutation(ADD_TODO)
...
addTodo({ variables: { type: input.value } })
{mutationLoading && <p>Loading...</p>}
{mutationError && <p>Error :( Please try again</p>}

更新缓存

如果一个mutation改动到了多个实体,apollo客户端缓存不会自动更新同步,在hook的第二个参数中使用update函数来修改缓存数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const [addTodo] = useMutation(ADD_TODO, {
// cache代表Apollo客户端缓存的对象
// 对象data传递mutation结果属性
update(cache, { data: { addTodo } }) {
// cache.modify直接修改缓存的字段
cache.modify({
fields: {
todos(existingTodos = []) {
// 写入缓存字段
const newTodoRef = cache.writeFragment({
data: addTodo,
fragment: gql`
fragment NewTodo on Todo {
id
type
}
`
})
return [...existingTodos, newTodoRef];
}
}
})
}
})

Snipaste_2021-04-11_20-06-55

Subscription(订阅)

服务端文档: https://www.apollographql.com/docs/apollo-server/data/subscriptions/

订阅可以保持与server连接(最常见是通过WebSocket方法)

1
npm install subscriptions-transport-ws

拆分通讯,查询走http连接,订阅走websocket,然后在初始化的时时候把splitLink加上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { split, HttpLink } from '@apollo/client'
import { getMainDefinition } from '@apollo/client/utilities'
import { WebSocketLink } from '@apollo/client/link/ws'
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql'
})
const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/subscriptions',
options: {
reconnect: true,
// 身份验证
connectionParams: {
authToken: user.authToken,
},
}
})
// 三个参数,1.调用执行函数2.为true时返回第二个参数3.false时返回第三个参数
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink,
)
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache()
})

使用场景:

  1. 对大对象进行小的,增量的修改.
  2. 低延迟,实时的场景.eg: 聊天室

不会主动响应,需要服务端发生特定事件才会推送,下面例子呈现博客的最近评论:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 服务端
type Subscription {
commentAdded(postID: ID!): Comment
}
// 客户端gql
const COMMENTS_SUBSCRIPTION = gql`
subscription OnCommentAdded($postID: ID!) {
commentAdded(postID: $postID) {
id
content
}
}
`
function LatestComment({ postID }) {
const { data: { commentAdded }, loading } = useSubscription(
COMMENTS_SUBSCRIPTION,
{ variables: { postID } }
);
return <h4>New comment: {!loading && commentAdded.content}</h4>;
}

Fragment(片段)

在操作之间共享查询逻辑字段

1
2
3
4
5
6
7
8
9
10
fragment NameParts on Person {
firstName
lastName
}
query GetPerson {
people(id: "7") {
...NameParts
avatar(size: LARGE)
}
}

上面的语法等于下面的

1
2
3
4
5
6
7
query GetPerson {
people(id: "7") {
firstName
lastName
avatar(size: LARGE)
}
}

也可以单独起一个fragment文件,引入的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const CORE_COMMENT_FIELDS = gql`
fragment CoreCommentFields on Comment {
id
postedBy {
username
displayName
}
}
`
...
import { CORE_COMMENT_FIELDS } from './fragments'
const GET_POST_DETAILS = gql`
${CORE_COMMENT_FIELDS}
query CommentsForPost($postId: ID!) {
post(postId: $postId) {
title
comments {
...CoreCommentFields
}
}
}
`