Vue 3 + vue-apollo 4 で GraphQL query を行う(Github の API を利用してリポジトリの取得/追加読み込みを行う)

Vue
Vueフロントエンド

Vue 3 + vue-apollo 4 を組み合わせた場合の使い方を説明します。
vue-apollo 3 と 4 では書き方が大きく変わっていますのでご注意ください。

バージョン

  • Vue 3.2.37
  • @vue/apollo-composable: 4.0.0-alpha.19
  • Vite 3.0.0

環境としては、https://kasyalog.site/blog/vite-vue-vuex-storybook-init/ で作った Vite + Vu3 環境をベースにしています。

セットアップ

vue-apollo は名前の通り、apollo を利用します。ので、まずは @apollo/client をインストール。

yarn add graphql graphql-tag @apollo/client

インストール後、ApolloClient のインスタンスを作成します。

import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core'

// HTTP connection to the API
const httpLink = createHttpLink({
  uri: 'https://api.github.com/graphql',
  fetch: (uri: RequestInfo, options: RequestInit) => {
    const headers = {
        Authorization: 'Bearer <Github Token>'
    };
    options.headers = headers
    return fetch(uri, options);
  },

})

// Cache implementation
const cache = new InMemoryCache()

// Create the apollo client
const apolloClient = new ApolloClient({
  link: httpLink,
  cache,
})

export default apolloClient

5行目の uri ではGarphQLのエンドポイントを指定します。ここでは、まずはフロントだけで確認できるよう、Github の GraphQL エンドポイントを指定しています。8行明の <Github Token> の部分は、https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token に従って、トークンを発行したものを指定すればOKです。

その後、vue apollo をインストールします。

// Comosition API
yarn add @vue/apollo-composable

main.ts は次のようになります。上記で生成したものを DefaultApolloClient として指定します。

import { createApp, provide, h } from "vue";
import { DefaultApolloClient } from "@vue/apollo-composable";

import apolloClient from "./plugins/apolloclient";

import "./style.css";
import App from "./App.vue";

createApp({
  setup() {
    provide(DefaultApolloClient, apolloClient);
  },

  render: () => h(App),
}).mount("#app");

これだけで準備は完了です。

GraphQL のクエリ実行

取得結果の表示(result)

準備ができたので、実際に Github に対してクエリを実行します。クエリを実行する HelloGraphQL.vue を作り、それを App.vue で表示させるようにしています。

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <HelloGraphQL />
</template>

<script setup lang="ts">
import HelloGraphQL from "./components/HelloGraphQL.vue";
</script>

HelloGraphQL.vue は以下になります。長いですが、特に難しいことはしておらず、ここでは、「vuejs」の Github organizations のうち、スター数が多いものから 50件を取得して一覧として表示させています。

<template>
  <div v-if="result">
    <table :border="1" style="border-collapse: collapse">
      <thead>
        <tr>
          <th>No</th>
          <th>name</th>
          <th>description</th>
          <th>createdAt</th>
          <th>star</th>
          <th>url</th>
        </tr>
      </thead>
      <tbody>
        <tr
          v-for="(v, index) in result.organization.repositories.nodes"
          :key="v.url"
        >
          <td>{{ index + 1 }}</td>
          <td>{{ v.name }}</td>
          <td>{{ v.description }}</td>
          <td>{{ new Date(v.createdAt).toLocaleDateString() }}</td>
          <td>{{ v.stargazers.totalCount }}</td>
          <td>{{ v.url }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts">
import { useQuery } from "@vue/apollo-composable";
import gql from "graphql-tag";

const { result } = useQuery(gql`
  query {
    organization(login: "vuejs") {
      repositories(first: 50, orderBy: { field: STARGAZERS, direction: DESC }) {
        nodes {
          name
          description
          createdAt
          updatedAt
          url
          stargazers {
            totalCount
          }
        }
      }
    }
  }
`);
</script>

なお、Github で指定ユーザの一覧を取得したい場合には、query { user(login: <ユーザ名>) { ...} } になります。organizationの場合は、query { organization(login: <組織名>) { ...} } です。
Github スター数は stargazers の totalCount で取得でき、これを orderBy することで多い順に取得ができます。

実行すると結果が取得できます。なお、上の通り、useQuery を記述した時点でリクエストは実行がされます。

Github の GraphQL クエリ実行結果 - vuejs organization のリポジトリ取得
Github の GraphQL クエリ実行結果 – vuejs organization のリポジトリ取得

簡単に GraphQL を使うことができます。useQuery は result 以外にも、loading(処理中), error(エラー有無) といった実際に開発するときには必須な情報も持っています。

読み込み中(loading), エラー(error)

取得状況やAPIエラー時も簡単に制御できます。loading は取得中のみ true になります。error は200以外の場合です。

以下は、loading, error を追加しつつ、エラーになるよう、取得件数を 50から101にします(Github では 101以上を指定するとエラーになります)。

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else-if="result">
  .... 以降は同じため省略
</template>

<script setup lang="ts">
import { useQuery } from "@vue/apollo-composable";
import gql from "graphql-tag";

const { result, loading, error } = useQuery(gql`
  query {
    organization(login: "vuejs") {
      repositories(first: 101, orderBy: { field: STARGAZERS, direction: DESC })
    ... 以降は同じため省略
  }
`);
</script>

一瞬ですが、取得中は「loading…」が表示されます。

Github の GraphQL クエリ実行 - 読込中は loading... を表示
Github の GraphQL クエリ実行 – 読込中は loading… を表示

エラーメッセージもちゃんと表示されています。

Github の GraphQL クエリ実行 - エラー時はエラーメッセージを表示
Github の GraphQL クエリ実行 – エラー時はエラーメッセージを表示

このように、vue-apollo を利用することで、クエリの実行も、読み込み中の判断も、エラー時のハンドリングも簡単に行うことができます。

パラメータの変数化

もちろんですが、通常のアプリケーションでは、クエリに指定するパラメータは動的です。userQuery の第2引数に指定することでパラメータが渡せます。以下では、select 要素を追加して、クエリに渡す情報を動的に変えるようにしました。画面表示時には初期値が vuejs なので同じ値が表示されます。

<template> 
  <select @change="(v) => (variables.login = v.target.value)">
    <option>vuejs</option>
    <option>facebook</option>
    <option>angular</option>
  </select>
  ... 以降は同じ
</template>

<script setup lang="ts">
import { useQuery } from "@vue/apollo-composable";
import gql from "graphql-tag";
import { ref } from "vue";

const variables = ref({
  login: "vuejs",
});

const { result, loading, error } = useQuery(
  gql`
    query ($login: String!) {
      organization(login: $login) {
        ... ここは同じ
      }
    }
  `,
  variables
);
</script>

画面表示後、select で値を変えると選択した organization の一覧を取得します(以下は angular の例)

Github の GraphQL クエリ実行 - 対象切り替え後に再取得
Github の GraphQL クエリ実行 – 対象切り替え後に再取得

ページネーション

変数化までいけたので、次はページネーションです。実際には利用する GraphQL サービスの実装によりますが、一般的なものは、https://relay.dev/graphql/connections.htm にある Relay Cursor と呼ばれる方式です。Github もこちらです。これに関しては調べたらたくさんでてきます。

やることは大きくは以下の3つです。これらはコード中にコメント記載しています。

  • クエリ実行時に cursor 位置を取得するようクエリ修正
  • 次を読み込む場合に上の cursor を指定して fetchMore を実行
  • 次を読み込んだ後は、前に取得した結果をマージする
<template>
  ...ここまで変更なし
    <button @click="loadNext()">追加読み込み</button>
  </div>
</template>

<script setup lang="ts">
import { useQuery } from "@vue/apollo-composable";
import gql from "graphql-tag";
import { ref } from "vue";

// パラメータに cursor を指定します。最初は先頭からなので null にします。
const variables = ref({
  login: "vuejs",
  cursor: null,
});

// 追加読み込み処理を行うために fetchMore を参照できるようにします。
const { result, loading, error, fetchMore } = useQuery(
  gql`
    // 上の cursor をクエリに指定します。指定した cursor から 50件を取得する動きになります。
    query ($login: String!, $cursor: String) {
      organization(login: $login) {
        repositories(
          first: 50
          after: $cursor
          orderBy: { field: STARGAZERS, direction: DESC }
        ) {
          nodes {
            name
            description
            createdAt
            updatedAt
            url
            stargazers {
              totalCount
            }
          }
  // ページ情報を取得します
          pageInfo {
            endCursor
            hasNextPage
          }
        }
      }
    }
  `,
  variables
);

// ボタン押下時の処理です
const loadNext = () => {
 // 追加取得をします。variables に指定するのは cursor などページ情報のみでOKです。
  // 再度 login を指定する必要はありません。 
  fetchMore({
    variables: {
      cursor: result.value.organization.repositories.pageInfo.endCursor,
    },

    // 取得後のコールバック処理です
    // 追加取得前の結果と、今回取得したデータが引き数にあります
    updateQuery: (previoiusResult, { fetchMoreResult }) => {
      const { nodes: newNodes, pageInfo: newPageInfo } =
        fetchMoreResult.organization.repositories;
      
      // キャッシュを更新した結果を返します。上のクエリに記載はありませんが、
      // __typename なども含むようにマージしないとエラーになります。
      // やっていることは、追加読み込みの一覧を付け足し、pageInfo は最新のものに置き換えをしています
      return {
        organization: {
          ...previoiusResult.organization,
          repositories: {
            ...previoiusResult.organization.repositories,
            nodes: [
              ...previoiusResult.organization.repositories.nodes,
              ...newNodes,
            ],
            pageInfo: newPageInfo,
          },
        },
      };
    },
  });
};
</script>

上記を実行すると最新の状態(1回追加読み込みすると100件)の一覧が表示されます。

Github の GraphQL クエリ実行 - 追加読込結果
Github の GraphQL クエリ実行 – 追加読込結果

データ取得、エラー時のカスタム処理

追加読み込み時 fetchMore の updateQuery のように、データ取得時に任意の処理を行うことも可能です。

onResult((queryResult) => {
  console.log("データ取得しました");
  console.log(queryResult); // 取得できたデータが参照できます
});
onError((queryResult) => {
  console.log("取得エラーです");
  console.log(queryResult); // エラー情報が参照できます
});

これでクエリの実行に関しては、基本的な部分は実行できるようになったと思います。その他にも vue-apollo には様々な機能があり、例えば 今回は useQuery を行うとすぐに API が実行されましたが、何らかの条件(例:クエリに渡すパラメータを先に取得する必要がある)があり、それを満たした後に実行させる、ということもオプションを使うと可能です。

思ったより長くなったので mutation は別の記事として記載したいと思います。

コメント