本地状态
为什么要使用 Apollo 的本地状态管理?
当你使用 Apollo 执行 GraphQL 查询时,API 调用的结果将存储在 Apollo 缓存中。现在想象一下,你还需要存储一些本地应用状态并使之可用于不同的组件。通常,在 Vue 应用中我们可以使用 Vuex 来实现这一需求。但同时使用 Apollo 和 Vuex 意味着你将数据存储到了两个不同的位置,导致你拥有两个数据源。
好在 Apollo 具有将本地应用数据存储到缓存的机制。以前,它使用了 apollo-link-state 库来实现。在 Apollo 2.5 发布后这个功能被包含在 Apollo 核心中。
创建一个本地 schema
如同创建 GraphQL schema 是在服务端定义数据模型的第一步一样,编写本地 schema 是我们在客户端进行的第一步。
让我们创建一个本地 schema 来描述将在 todo 列表中作为单个事项的元素。在这个事项中应该有一些文本、一些定义它是否已经完成的属性、以及一个 ID 来区分不同的待办事项。所以,它应该是一个具有三个属性的对象:
{
id: 'uniqueId',
text: 'some text',
done: false
}
现在我们可以将 Item
类型添加到本地 GraphQL schema 中。
//main.js
import gql from 'graphql-tag';
export const typeDefs = gql`
type Item {
id: ID!
text: String!
done: Boolean!
}
`;
这里的 gql
代表解析 GraphQL 查询字符串的 JavaScript 模板字符串标签。
现在我们需要将 typeDefs
添加到我们的 Apollo 客户端。
// main.js
const apolloClient = new ApolloClient({
typeDefs,
resolvers: {},
});
WARNING
正如你所见,我们在此处添加了一个空的 resolvers
对象:如果我们不将它添加到 Apollo 客户端的选项,它将无法识别对本地状态的查询,并将尝试向远程 URL 发送请求。
在本地扩展一个远程 GraphQL schema
除了从零开始创建本地 schema 之外,你还可以将本地的虚拟字段添加到现有的远程 schema 中。这些字段仅存在于客户端上,可用于使用本地状态装饰服务端数据。
想象我们的远程 schema 中有一个 User
类型:
type User {
name: String!
age: Int!
}
而我们想要给 User
添加一个只在本地拥有的属性:
export const schema = gql`
extend type User {
twitter: String
}
`;
现在,在查询用户时,我们需要指定 twitter
是本地字段:
const userQuery = gql`
user {
name
age
twitter @client
}
`;
初始化 Apollo 缓存
要在应用中初始化 Apollo 缓存,需要使用 InMemoryCache
构造函数。首先,将它导入你的主文件:
// main.js
import ApolloClient from 'apollo-boost';
import { InMemoryCache } from 'apollo-cache-inmemory';
const cache = new InMemoryCache();
现在我们需要在 Apollo 客户端选项中添加缓存:
//main.js
const apolloClient = new ApolloClient({
cache,
typeDefs,
resolvers: {},
});
现在缓存是空的。要向缓存添加一些初始数据,我们需要使用 writeData
方法:
// main.js
const apolloClient = new ApolloClient({
cache,
typeDefs,
resolvers: {},
});
cache.writeData({
data: {
todoItems: [
{
__typename: 'Item',
id: 'dqdBHJGgjgjg',
text: 'test',
done: true,
},
],
},
});
我们刚刚为缓存数据添加了一个 todoItems
数组,并定义了其中每项的类型名称为 Item
(在我们的本地 schema 中指定)。
查询本地数据
查询本地缓存与 将 GraphQL 查询发送到远程服务器 非常相似。首先,我们需要创建一个查询:
// App.vue
import gql from 'graphql-tag';
const todoItemsQuery = gql`
{
todoItems @client {
id
text
done
}
}
`;
@client
指令是与发送到远程 API 的查询的主要区别。该指令指定 Apollo 客户端不应在远程 GraqhQL API 上执行此查询,而是应该从本地缓存中获取结果。
现在,我们可以在 Vue 组件中像普通的 Apollo 查询一样使用此查询:
// App.vue
apollo: {
todoItems: {
query: todoItemsQuery
}
},
使用变更修改本地数据
我们有两种不同的方法来修改本地数据:
- 使用
writeData
方法直接写入,就像我们在 缓存初始化 时所做的那样; - 调用一个 GraphQL 变更。
让我们在 本地 GraphQL schema 中添加一些变更:
// main.js
export const typeDefs = gql`
type Item {
id: ID!
text: String!
done: Boolean!
}
type Mutation {
checkItem(id: ID!): Boolean
addItem(text: String!): Item
}
`;
checkItem
变更会将某一事项的 done
属性设置为相反的布尔值。让我们用 gql
创建它:
// App.vue
const checkItemMutation = gql`
mutation($id: ID!) {
checkItem(id: $id) @client
}
`;
我们定义了一个本地变更(因为在这里写了一个 @client
指令),它将接受一个唯一的标识符作为参数。现在,我们需要一个解析器:一个解析 schema 中类型或字段的值的函数。
在我们的例子中,解析器将定义当我们执行了变更时会对本地 Apollo 缓存做出哪些更改。本地解析器具有与远程解析器相同的功能签名((parent, args, context, info) => data
)。事实上,我们只需要使用 args(传递给变更的参数)和 context(我们需要它的缓存属性来读写数据)。
让我们在主文件中添加一个解析器:
// main.js
const resolvers = {
Mutation: {
checkItem: (_, { id }, { cache }) => {
const data = cache.readQuery({ query: todoItemsQuery });
const currentItem = data.todoItems.find(item => item.id === id);
currentItem.done = !currentItem.done;
cache.writeQuery({ query: todoItemsQuery, data });
return currentItem.done;
},
};
在这里我们做了什么?
- 从缓存中读取
todoItemsQuery
以查看我们现在拥有的todoItems
; - 查找具有给定 id 的事项;
- 将找到的事项的
done
属性改为相反的值; - 将我们更改的
todoItems
写回缓存; - 将
done
属性作为变更的结果返回。
现在我们需要用新创建的 resolvers
替换 Apollo 客户端选项中的空 resolvers
对象:
// main.js
const resolvers = {
Mutation: {
checkItem: (_, { id }, { cache }) => {
const data = cache.readQuery({ query: todoItemsQuery });
const currentItem = data.todoItems.find(item => item.id === id);
currentItem.done = !currentItem.done;
cache.writeQuery({ query: todoItemsQuery, data });
return currentItem.done;
},
};
const apolloClient = new ApolloClient({
cache,
typeDefs,
resolvers,
});
现在,我们可以在 Vue 组件中像普通的 Apollo 变更 一样使用此变更:
// App.vue
methods: {
checkItem(id) {
this.$apollo.mutate({
mutation: checkItemMutation,
variables: { id }
});
},
}