Vue Apollo v4:初体验
本文要求您已经熟悉 GraphQL、Apollo Client 和 Vue 的基础知识。在此声明:我之前在“如何在 Apollo 中使用 Vue”的演讲中已经尝试过介绍这些内容。我们还将使用 Vue Composition API。如果您不熟悉这个概念,我强烈建议您阅读Vue Composition API RFC。
几周前,vue-apollo(集成了 Apollo 客户端的 Vue.js 版本)发布了 4.0 alpha 版本,我立刻决定尝试一下。这个版本有什么特别之处呢?除了现有的 API 之外,它还基于 Vue Composition API 添加了可组合项选项。我之前使用过vue-apollo ,所以决定测试一下新 API 与之前的版本相比有什么区别。
我们将要使用的示例
为了探索新的 API,我将使用我在 Vue+Apollo 演讲中已经展示过的一个示例——我称之为“Vue Heroes”。这是一个简洁的应用程序,它有一个查询语句用于从 GraphQL API 中获取所有英雄数据,以及两个修改语句:一个用于添加英雄数据,一个用于删除英雄数据。界面如下所示:
您可以在此处找到包含旧选项 API 的源代码。其中包含 GraphQL 服务器;您需要运行它才能使应用程序正常运行。
yarn apollo
现在让我们开始将其重构为新版本。
安装
第一步,我们可以安全地从项目中删除旧版本的vue-apollo :
yarn remove vue-apollo
我们需要安装一个新的。从版本 4 开始,我们可以选择要使用的 API,并仅安装所需的包。在本例中,我们想尝试一种新的可组合语法:
yarn add @vue/apollo-composable
Composition API 是 Vue 3 的一部分,目前尚未发布。幸运的是,我们可以使用一个独立的库使其也能与 Vue 2 兼容,因此目前我们也需要安装它:
yarn add @vue/composition-api
现在,让我们打开src/main.js
文件并进行一些更改。首先,我们需要将 Composition API 插件添加到我们的 Vue 应用程序中:
// main.js
import VueCompositionApi from "@vue/composition-api";
Vue.use(VueCompositionApi);
我们需要使用新库设置一个 Apollo 客户端apollo-composable
。让我们定义一个指向 GraphQL 端点的链接,并创建一个缓存,以便稍后将它们传递给客户端构造函数:
// main.js
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
const httpLink = createHttpLink({
uri: "http://localhost:4000/graphql"
});
const cache = new InMemoryCache();
现在,我们可以创建一个 Apollo Client 实例:
// main.js
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
const httpLink = createHttpLink({
uri: "http://localhost:4000/graphql"
});
const cache = new InMemoryCache();
const apolloClient = new ApolloClient({
link: httpLink,
cache
});
创建客户端与之前的 Vue Apollo 版本并没有什么不同,而且目前为止它实际上与 Vue 没有任何关系——我们只是设置了一个 Apollo 客户端本身。不同的是,我们不再需要创建客户端apolloProvider
了!我们只需原生地为 Vue 应用程序提供一个客户端,而无需 ApolloProvider 实例:
// main.js
import { provide } from "@vue/composition-api";
import { DefaultApolloClient } from "@vue/apollo-composable";
new Vue({
setup() {
provide(DefaultApolloClient, apolloClient);
},
render: h => h(App)
}).$mount("#app");
provide
这里是从@vue/composition-api
包中导入的,它启用了类似于 2.xprovide/inject
选项的依赖注入。第一个参数provide
是键,第二个参数是值
添加查询
为了在页面上显示 Vue 英雄列表,我们需要创建allHeroes
查询:
// graphql/allHeroes.query.gql
query AllHeroes {
allHeroes {
id
name
twitter
github
image
}
}
我们将在我们的App.vue
组件中使用它,因此让我们将它导入到那里:
// App.vue
import allHeroesQuery from "./graphql/allHeroes.query.gql";
通过 Options API,我们在 Vue 组件apollo
属性中使用了此查询”:
// App.vue
name: "app",
data() {...},
apollo: {
allHeroes: {
query: allHeroesQuery,s
}
}
现在我们将进行修改App.vue
,使其能够与 Composition API 兼容。实际上,它需要在现有组件中添加一个选项 - a setup
:
// App.vue
export default {
name: "app",
setup() {},
data() {...}
在这里,setup
我们将在函数中使用vue-apollo可组合项,并需要返回结果以便在模板中使用它们。我们的第一步是获取allHeroes
查询结果,因此我们需要导入第一个可组合项并将 GraphQL 查询传递给它:
// App.vue
import allHeroesQuery from "./graphql/allHeroes.query.gql";
import { useQuery } from "@vue/apollo-composable";
export default {
name: "app",
setup() {
const { result } = useQuery(allHeroesQuery);
return { result }
},
data() {...}
useQuery
最多可以接受三个参数:第一个是包含查询的 GraphQL 文档,第二个是变量对象,第三个是查询选项。在本例中,我们使用默认选项,不需要向查询传递任何变量,因此我们只传递第一个参数。
这里是什么result
?它与名称完全匹配——它是 GraphQL 查询的结果,包含allHeroes
数组,但它也是一个响应式对象——所以它是一个 Vue ref
。这就是为什么它将结果数组包装在value
属性中:
由于 Vue 在模板中为我们自动展开,我们可以简单地迭代result.allHeroes
来呈现列表:
<template v-for="hero in result.allHeroes">
undefined
但是,由于结果仍在从 API 加载,因此此数组的初始值将是。我们可以在此处添加一个检查,以确保我们已经有一个结果,例如result && result.allHeroes
,但 v4 有一个实用的辅助函数可以帮我们完成此操作 - useResult
。它是一个很棒的实用程序,可以帮助您塑造从 API 获取的结果,尤其是在您需要获取一些深层嵌套的数据或从一个查询中获取几个不同的结果时非常有用:
<template v-for="hero in allHeroes">
<script>
import { useQuery, useResult } from "@vue/apollo-composable";
export default {
setup() {
const { result } = useQuery(allHeroesQuery);
const allHeroes = useResult(result, null, data => data.allHeroes)
return { allHeroes }
},
}
</script>
useResult
接受三个参数:GraphQL 查询的结果、默认值(null
在我们的例子中),以及一个选择函数,该函数返回我们想要从结果对象中检索的数据。如果结果只包含一个属性(就像allHeroes
我们的例子中那样),我们可以稍微简化一下:
// App.vue
setup() {
const { result } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
return { allHeroes }
},
剩下的就是在我们实际从 API 获取数据时显示加载状态。除了 之外result
,还useQuery
可以返回:loading
// App.vue
setup() {
const { result, loading } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
return { allHeroes, loading }
},
我们可以在模板中有条件地渲染它:
<h2 v-if="loading">Loading...</h2>
让我们将 v3 的代码与新代码进行比较:
虽然新语法更加冗长,但也更加可定制(为了调整响应,我们需要update
在 v3 语法中添加一个属性)。我喜欢我们能够loading
为每个查询正确地暴露它,而不是将其用作全局$apollo
对象的嵌套属性。
处理突变
现在让我们重构一下新语法的突变。在这个应用中,我们有两个突变:一个是添加新英雄,另一个是删除现有英雄:
// graphql/addHero.mutation.gql
mutation AddHero($hero: HeroInput!) {
addHero(hero: $hero) {
id
twitter
name
github
image
}
}
// graphql/deleteHero.mutation.gql
mutation DeleteHero($name: String!) {
deleteHero(name: $name)
}
在 Options API 语法中,我们将 Mutation 作为 Vue 实例属性的方法调用$apollo
:
this.$apollo.mutate({
mutation: mutationName,
})
让我们从第addHero
一个开始重构。与查询类似,我们需要将变更导入到,App.vue
并将其作为参数传递给useMutation
可组合函数:
// App.vue
import addHeroMutation from "./graphql/addHero.mutation.gql";
import { useQuery, useResult, useMutation } from "@vue/apollo-composable";
export default {
setup() {
const { result, loading } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
const { mutate } = useMutation(addHeroMutation)
},
}
这里mutate
实际上是我们需要调用一个方法来将变更发送到我们的 GraphQL API 端点。但是,在变更的情况下addHero
,我们还需要发送一个变量hero
来定义我们想要添加到列表中的英雄。好消息是,我们可以从setup
函数中返回此方法,并在 Options API 方法中使用它。由于我们将有两个变更,因此我们还需要重命名该mutate
函数,因此为其指定一个更直观的名称是个好主意:
// App.vue
setup() {
const { result, loading } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
const { mutate: addNewHero } = useMutation(addHeroMutation)
return { allHeroes, loading, addNewHero }
},
addHero
现在我们可以在组件中已经存在的方法中调用它:
export default {
setup() {...},
methods: {
addHero() {
const hero = {
name: this.name,
image: this.image,
twitter: this.twitter,
github: this.github,
github: this.github
};
this.addNewHero({ hero });
}
}
}
如你所见,我们在调用mutation时传递了一个变量。还有另一种方法,我们也可以将变量添加到options对象,并将其useMutation
作为第二个参数传递给函数:
const { mutate: addNewHero } = useMutation(addHeroMutation, {
variables: {
hero: someHero
}
})
现在,我们的变更将成功发送到 GraphQL 服务器。不过,我们还需要在成功响应后更新本地 Apollo 缓存——否则,英雄列表在我们重新加载页面之前不会更改。因此,我们还需要allHeroes
从 Apollo 缓存中读取查询,更改列表并添加新英雄,然后将其写回。我们将在函数中执行此操作(我们可以像使用 一样update
将其与参数一起传递):options
variables
// App.vue
setup() {
const { result, loading } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
const { mutate: addNewHero } = useMutation(addHeroMutation, {
update: (cache, { data: { addHero } }) => {
const data = cache.readQuery({ query: allHeroesQuery });
data.allHeroes = [...data.allHeroes, addHero];
cache.writeQuery({ query: allHeroesQuery, data });
}
})
return { allHeroes, loading, addNewHero }
},
那么,当我们添加新英雄时,加载状态是怎样的呢?在 v3 中,我们通过创建一个外部标志并将其更改为 来实现finally
:
// App.vue
export default {
data() {
return {
isSaving: false
};
},
methods: {
addHero() {
...
this.isSaving = true;
this.$apollo
.mutate({
mutation: addHeroMutation,
variables: {
hero
},
update: (store, { data: { addHero } }) => {
const data = store.readQuery({ query: allHeroesQuery });
data.allHeroes.push(addHero);
store.writeQuery({ query: allHeroesQuery, data });
}
})
.finally(() => {
this.isSaving = false;
});
}
}
}
在 v4 组合 API 中,我们可以简单地从函数返回给定突变的加载状态useMutation
:
setup() {
...
const { mutate: addNewHero, loading: isSaving } = useMutation(
addHeroMutation,
{
update: (cache, { data: { addHero } }) => {
const data = cache.readQuery({ query: allHeroesQuery });
data.allHeroes = [...data.allHeroes, addHero];
cache.writeQuery({ query: allHeroesQuery, data });
}
}
);
return {
...
addNewHero,
isSaving
};
}
让我们比较一下 v3 和 v4 组合 API 的代码:
在我看来,组合 API 代码变得更加结构化,并且它也不需要外部标志来保持加载状态。
deleteHero
突变可以用非常类似的方式重构,除了一个要点:在update
函数中,我们需要删除通过名称找到的英雄,而该名称仅在模板中可用(因为我们用v-for
指令迭代英雄列表,并且无法跳出hero.name
循环)。这就是为什么我们需要在调用突变的地方直接在 options 参数中v-for
传递一个函数:update
<vue-hero
v-for="hero in allHeroes"
:hero="hero"
@deleteHero="
deleteHero(
{ name: $event },
{
update: cache => updateHeroAfterDelete(cache, $event)
}
)
"
:key="hero.name"
></vue-hero>
<script>
export default {
setup() {
...
const { mutate: deleteHero } = useMutation(deleteHeroMutation);
const updateHeroAfterDelete = (cache, name) => {
const data = cache.readQuery({ query: allHeroesQuery });
data.allHeroes = data.allHeroes.filter(hero => hero.name !== name);
cache.writeQuery({ query: allHeroesQuery, data });
};
return {
...
deleteHero,
updateHeroAfterDelete,
};
}
}
</script>
结论
我非常喜欢 vue-apollo v4 可组合组件提供的代码抽象级别。无需创建Vue 实例provider
并注入$apollo
对象,在单元测试中模拟 Apollo 客户端会更加容易。代码也感觉更加结构化和直观。我会等待正式发布,并在实际项目中试用!