Vue Apollo v4:初体验

2025-05-25

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
Enter fullscreen mode Exit fullscreen mode

现在让我们开始将其重构为新版本。

安装

第一步,我们可以安全地从项目中删除旧版本的vue-apollo :

yarn remove vue-apollo
Enter fullscreen mode Exit fullscreen mode

我们需要安装一个新的。从版本 4 开始,我们可以选择要使用的 API,并仅安装所需的包。在本例中,我们想尝试一种新的可组合语法:

yarn add @vue/apollo-composable
Enter fullscreen mode Exit fullscreen mode

Composition API 是 Vue 3 的一部分,目前尚未发布。幸运的是,我们可以使用一个独立的库使其也能与 Vue 2 兼容,因此目前我们也需要安装它:

yarn add @vue/composition-api
Enter fullscreen mode Exit fullscreen mode

现在,让我们打开src/main.js文件并进行一些更改。首先,我们需要将 Composition API 插件添加到我们的 Vue 应用程序中:

// main.js

import VueCompositionApi from "@vue/composition-api";

Vue.use(VueCompositionApi);
Enter fullscreen mode Exit fullscreen mode

我们需要使用新库设置一个 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();
Enter fullscreen mode Exit fullscreen mode

现在,我们可以创建一个 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
});
Enter fullscreen mode Exit fullscreen mode

创建客户端与之前的 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");
Enter fullscreen mode Exit fullscreen mode

provide这里是从@vue/composition-api包中导入的,它启用了类似于 2.xprovide/inject选项的依赖注入。第一个参数provide是键,第二个参数是值

3.x 4.x 可组合项语法
3.x 4.x

添加查询

为了在页面上显示 Vue 英雄列表,我们需要创建allHeroes查询:

// graphql/allHeroes.query.gql

query AllHeroes {
  allHeroes {
    id
    name
    twitter
    github
    image
  }
}
Enter fullscreen mode Exit fullscreen mode

我们将在我们的App.vue组件中使用它,因此让我们将它导入到那里:

// App.vue

import allHeroesQuery from "./graphql/allHeroes.query.gql";
Enter fullscreen mode Exit fullscreen mode

通过 Options API,我们在 Vue 组件apollo属性中使用了此查询”:

// App.vue

  name: "app",
  data() {...},
  apollo: {
    allHeroes: {
      query: allHeroesQuery,s
    }
  }
Enter fullscreen mode Exit fullscreen mode

现在我们将进行修改App.vue,使其能够与 Composition API 兼容。实际上,它需要在现有组件中添加一个选项 - a setup

// App.vue

export default {
  name: "app",
  setup() {},
  data() {...}
Enter fullscreen mode Exit fullscreen mode

在这里,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() {...}
Enter fullscreen mode Exit fullscreen mode

useQuery最多可以接受三个参数:第一个是包含查询的 GraphQL 文档,第二个是变量对象,第三个是查询选项。在本例中,我们使用默认选项,不需要向查询传递任何变量,因此我们只传递第一个参数。

这里是什么result?它与名称完全匹配——它是 GraphQL 查询的结果,包含allHeroes数组,但它也是一个响应式对象——所以它是一个 Vue ref。这就是为什么它将结果数组包装在value属性中:

结果结构

由于 Vue 在模板中为我们自动展开,我们可以简单地迭代result.allHeroes来呈现列表:

<template v-for="hero in result.allHeroes">
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

useResult接受三个参数:GraphQL 查询的结果、默认值(null在我们的例子中),以及一个选择函数,该函数返回我们想要从结果对象中检索的数据。如果结果只包含一个属性(就像allHeroes我们的例子中那样),我们可以稍微简化一下:

// App.vue

setup() {
  const { result } = useQuery(allHeroesQuery);
  const allHeroes = useResult(result)

  return { allHeroes }
},
Enter fullscreen mode Exit fullscreen mode

剩下的就是在我们实际从 API 获取数据时显示加载状态。除了 之外resultuseQuery可以返回:loading

// App.vue
setup() {
  const { result, loading } = useQuery(allHeroesQuery);
  const allHeroes = useResult(result)

  return { allHeroes, loading }
},
Enter fullscreen mode Exit fullscreen mode

我们可以在模板中有条件地渲染它:

<h2 v-if="loading">Loading...</h2>
Enter fullscreen mode Exit fullscreen mode

让我们将 v3 的代码与新代码进行比较:

3.x 4.x 可组合项语法
3.x 4.x

虽然新语法更加冗长,但也更加可定制(为了调整响应,我们需要update在 v3 语法中添加一个属性)。我喜欢我们能够loading为每个查询正确地暴露它,而不是将其用作全局$apollo对象的嵌套属性。

处理突变

现在让我们重构一下新语法的突变。在这个应用中,我们有两个突变:一个是添加新英雄,另一个是删除现有英雄:

// graphql/addHero.mutation.gql

mutation AddHero($hero: HeroInput!) {
  addHero(hero: $hero) {
    id
    twitter
    name
    github
    image
  }
}
Enter fullscreen mode Exit fullscreen mode
// graphql/deleteHero.mutation.gql

mutation DeleteHero($name: String!) {
  deleteHero(name: $name)
}
Enter fullscreen mode Exit fullscreen mode

在 Options API 语法中,我们将 Mutation 作为 Vue 实例属性的方法调用$apollo

this.$apollo.mutate({
  mutation: mutationName,
})
Enter fullscreen mode Exit fullscreen mode

让我们从第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)
  },
}
Enter fullscreen mode Exit fullscreen mode

这里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 }
},
Enter fullscreen mode Exit fullscreen mode

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 });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

如你所见,我们在调用mutation时传递了一个变量。还有另一种方法,我们也可以将变量添加到options对象,并将其useMutation作为第二个参数传递给函数:

const { mutate: addNewHero } = useMutation(addHeroMutation, {
  variables: {
    hero: someHero
  }
})
Enter fullscreen mode Exit fullscreen mode

现在,我们的变更将成功发送到 GraphQL 服务器。不过,我们还需要在成功响应后更新本地 Apollo 缓存——否则,英雄列表在我们重新加载页面之前不会更改。因此,我们还需要allHeroes从 Apollo 缓存中读取查询,更改列表并添加新英雄,然后将其写回。我们将在函数中执行此操作(我们可以像使用 一样update将其与参数一起传递):optionsvariables

// 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 }
},
Enter fullscreen mode Exit fullscreen mode

那么,当我们添加新英雄时,加载状态是怎样的呢?在 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;
        });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

在 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
  };
}
Enter fullscreen mode Exit fullscreen mode

让我们比较一下 v3 和 v4 组合 API 的代码:

3.x 4.x 可组合项语法
3.x 4.x

在我看来,组合 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>
Enter fullscreen mode Exit fullscreen mode

结论

我非常喜欢 vue-apollo v4 可组合组件提供的代码抽象级别。无需创建Vue 实例provider并注入$apollo对象,在单元测试中模拟 Apollo 客户端会更加容易。代码也感觉更加结构化和直观。我会等待正式发布,并在实际项目中试用!

文章来源:https://dev.to/n_tepluhina/vue-apollo-v4-the-first-look-c32
PREV
Node 与 Go:API 对决
NEXT
使用 Vue 构建桌面应用程序:Vuido