9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GraphQLとVue3を使ってポケモンのデータを取得してみる

Posted at

概要

現在弊社で開発中のサービス要件を満たす技術選定の際に、データ取得の方法としてRESTFul APIの他に選択肢としてGraphQLが挙げられることがありました。

その時点でのGraphQLの理解がJSON(のようなもの?)を返すAPIみたいなものという
なんともふわっとしたものだったので、これはいけないと思い年末年始に少し触ってみました。

開発環境

  • yarn 1.22.19
  • Typescript 4.9.3
  • Vue.js 3.2.45
  • vite 4.0.0
  • graphql 16.6.0
    etc...

GraphQLとは

GraphQL公式

GraphQLとはAPI向けのクエリ言語です。

CircleCIブログGraphQLとは?メリットや概要を入門ガイドで学ぶでは以下のようにまとめられています。

  • GraphQLは、クライアントアプリケーションが必要なデータをAPIからフェッチするために設計された言語です。
  • GraphQLを使用すると、クライアントアプリケーションはバックエンドAPIから必要なデータの型とシェイプを取得できます。
  • GraphQLでは、どんなタイプのリクエストでも、クライアント アプリケーションが呼び出すエンドポイントは1つだけです。
  • GraphQLはSQLとよく似ていますが、フロントエンドで機能します。

なんとなく分かってきましたが、まだ使い所がよく分かりませんね。
GraphQLの比較対象としてRESTFul APIが挙げられますが、それぞれどのように違うのか、以下で説明します。

RESTFul APIとの違い

RestFul API

詳細はRESTful API とはに記載がありますが、特徴の1つとして取得するリソースを特定するために、URIやGET・POSTなどリクエストメソッド等の情報が必要です。

従って、複数のリソースを取得するには複数のリクエスト(URIなど)が必要となります。

以下にRESTFul APIのリソース取得フローを示します。
図鑑No.25のポケモンの名前と種族値を取得しています。

Responce①.json
{
    "id": 25,
    "name": "Pikachu"
}
Responce②.json
{
    "hitPoints": 35,
    "attack": 55,
    "defense": 40,
    "specialAttack": 50,
    "specialDefense": 50,
    "speed": 90
}

GraphQL

次に、GraphQLで同じデータを取得するフローを示します。
エンドポイントが1つのみであり、取得するリソースはクエリによって決定します。

POSTするクエリは以下の通りです。

query{
    pokemon(id: 25) {
        id
        name
        basestats {
            hitPoints
            attack
            defense
            specialAttack
            specialDefense
            speed
        }
    }
}
Responce.json
    "pokemon" {
        "id": 25,
        "name": "Pikachu",
        "basestats": {
            "hitPoints": 35,
            "attack": 55,
            "defense": 40,
            "specialAttack": 50,
            "specialDefense": 50,
            "speed": 90
        }
    }

このようにGraphQL APIでは1回のリクエストでアプリが必要とするすべてのデータを取得することができます。

GraphQLを利用する際は、関連する必要なデータを1回で取得するクエリを作り、それをPOSTします。
上記の例では、API呼び出しを2つ使うのではなく、APIへのリクエスト1つでポケモンと種族値をネストされたJSONオブジェクトとして取得しています。

どのような時に使用するか

上記で示した通り、GraphQLの大きな利点は

  • クエリによって、欲しいデータのみを全て取得することができる
  • その際のAPIへのリクエストは一度だけでよい
  • エンドポイントが単一である

ということです。

例えば、関連するデータが多くRESTFul APIでは何回も呼び出しが必要な場合は、GraphQLの利用を検討するとよいでしょう。
呼び出し回数が削減され、サービスのパフォーマンス向上が期待できます。

Vue.jsからGraphqlを利用してデータ取得・絞り込み検索を実装

それでは、実際に使用してみます。
このあたりの記事が参考になりました。
GraphQL APIをVueJSフロントエンドに接続する方法

今回は練習として、ポケモンの名前、タイプ、画像を取得し、表示します。

GraphQLのエンドポイントとして、Pokemon APIのものを使用します。

https://graphql-pokemon2.vercel.app

ちなみに、練習用に使用できるGraphQL APIはこちらにあります。
8 Free to Use GraphQL APIs for Your Projects and Demos

GraphQL Playgroundに上記のエンドポイントを入力してみましょう。
すると、以下のようにSchema定義などが確認できます。

type Attack {
  name: String
  type: String
  damage: Int
}

type Pokemon {
  id: ID!
  number: String
  name: String
  weight: PokemonDimension
  height: PokemonDimension
  classification: String
  types: [String]
  resistant: [String]
  attacks: PokemonAttack
  weaknesses: [String]
  fleeRate: Float
  maxCP: Int
  evolutions: [Pokemon]
  evolutionRequirements: PokemonEvolutionRequirement
  maxHP: Int
  image: String
}

type PokemonAttack {
  fast: [Attack]
  special: [Attack]
}

type PokemonDimension {
  minimum: String
  maximum: String
}

type PokemonEvolutionRequirement {
  amount: Int
  name: String
}

type Query {
  query: Query
  pokemons(first: Int!): [Pokemon]
  pokemon(id: String, name: String): Pokemon
}

これを参考に実装を進めます。
まずは、以下のようにapollo関連のパッケージをインストールします。

Apolloは、アプリでGraphQLを使うためのツール群であり、コミュニティの取り組みでもあります。
Vue Apollo

yarn add vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-cache-inmemory graphql-tag @vue/apollo-composable

最終的なpackage.jsonは以下の通りです。

package.json
{
  "name": "vue-graphql",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@types/express": "^4.17.15",
    "@vue/apollo-composable": "^4.0.0-beta.1",
    "apollo-cache-inmemory": "^1.6.6",
    "apollo-client": "^2.6.10",
    "apollo-link": "^1.2.14",
    "apollo-link-http": "^1.5.17",
    "express": "^4.18.2",
    "express-graphql": "^0.12.0",
    "graphql": "^16.6.0",
    "graphql-tag": "^2.12.6",
    "vue": "^3.2.45",
    "vue-apollo": "^3.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.0.0",
    "typescript": "^4.9.3",
    "vite": "^4.0.0",
    "vue-tsc": "^1.0.11"
  }
}

次に、GraphQLを使用する準備をします。

main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import VueApollo from 'vue-apollo';

// GraphQL APIへの接続を確立
const httpLink = new HttpLink({
  uri: 'https://graphql-pokemon2.vercel.app'
});

// ApolloClientインスタンス生成
export const apolloClient = new ApolloClient({
  // データが正しいAPIからポーリングされるように設定
  link: httpLink,
  // キャッシュを渡す。InMemoryCacheはApolloClientのデフォルトのキャッシュ実装であるのでこれを使用
  cache: new InMemoryCache(),
  // Apollo Client Devtoolsを、Webブラウザのインスペクタに「Apollo」タブとして表示する
  connectToDevTools: true
});

const apolloProvider = new VueApollo({
  defaultClient: apolloClient
});


const app = createApp(App);
// VueApolloをvueで使用する
app.use(apolloProvider);
app.mount('#app');

今回取得するポケモンのデータの型定義をしておきます。

pokemon.ts
export type Pokemon =  {
    name: string;
    types: string[];
    image: string;
}

検索フォームのコンポーネントをApp.vueから読み込みます。

App.vue
<script setup lang="ts">
import PokemonForm from './components/PokemonForm.vue';
</script>

<template>
  <div>ポケモン検索したい!</div>
  <pokemon-form />
</template>

検索フォームを、ポケモンの名前(英語表記)での絞り込みができるよう実装します。

コンポーネントのsetup()外でuseQuery()を使用したい場合、
provideApolloClient(apolloClient);が必要です。
ドキュメントによるとこれは、Vueのprovide/injectメカニズムでクライアントをインジェクトできないからのようです。
Usage outside of setup

使用しない場合は以下のエラーが発生します。
Uncaught Error: Apollo client with id default not found. Use provideApolloClient() if you are outside of a component setup.

PokemonForm.vue
<template>
  <form>
    <input type="text" placeholder="Name" v-model="name">
    <input type="button" value="検索" @click="(e) => {pokemonQuery(name, e)}">
  </form>
  <pokemon-list v-if="fetchdata" :pokemons="fetchdata.pokemons" />
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useQuery, provideApolloClient } from '@vue/apollo-composable';
import gql from "graphql-tag"
import { apolloClient } from '../main';
import PokemonList from "./PokemonList.vue"
import { Pokemon } from '../model/pokemon';

const name = ref<string>();

type Query = {
  pokemons: Pokemon[]
} | undefined;

const fetchdata = ref<Query>();

// https://v4.apollo.vuejs.org/guide-composable/setup.html#usage-outside-of-setup
// 
provideApolloClient(apolloClient);

const getPokemonByName = (name: string) => {
  console.log("getpPokemonByName");
  const query =  gql`
  query($name: String) {
    pokemon(name: $name) {
      name
      types
      image
    }
  }
  `;

  const {onResult} = useQuery<Query>(query, {
    name
  });

  onResult(result => {
    fetchdata.value = {pokemons: [result.data.pokemon]};
  });
};

const getAllPokemons = () => {
  const query =  gql`
    query {
      pokemons(first: 150){
        name
        types
        image
      }
    }
    `;

  const {onResult} = useQuery<Query>(query);

  onResult(result => {
    fetchdata.value = result.data;
  });
};

const pokemonQuery = (name: string | undefined, event: MouseEvent) => {
  event.preventDefault();
  if (!name) {
    getAllPokemons();
    return;
  };
  getPokemonByName(name);
};

getAllPokemons();

</script>

このとき、結果の取得とfetchdataの更新は以下のように行なっています。

  const {onResult} = useQuery<Query>(query, {
    name
  });

  onResult(result => {
    fetchdata.value = {pokemons: [result.data.pokemon]};
  });

注意点として、以下のようにresultプロパティを取得することも可能ですが、fetchdataの更新ができずundefinedとなってしまいます。
これは、useQueryによるデータ取得は非同期処理であるため、データ取得が完了する前に代入処理を実行してしまうからです。

  hogehoge() => {
    const {result} = useQuery<Query>(query, {
      name
    });
    fetchdata.value = {pokemons: [result.data.pokemon]};
  }

次に、ポケモンのリスト表示用コンポーネントを実装します。
初回表示時には、150匹のポケモンを表示するためポケモン1種類ごとにループさせています。

PokemonList.vue
<script setup lang="ts">
import pokemonItem from './pokemonItem.vue';
import { Pokemon } from '../model/pokemon';

const {pokemons} = defineProps<{pokemons: Pokemon[]}>();
</script>

<template>
  <div v-for="pokemon in pokemons">
    <pokemon-item :pokemon="pokemon" />
  </div>
</template>

最後にポケモン1種類を表示するコンポーネントを実装します。
タイプはStringの配列であるため、join()で連結して表示しています。

PokemonItem.vue
<script setup lang="ts">
import { Pokemon } from '../model/pokemon';

const {pokemon} = defineProps<{pokemon: Pokemon}>();

</script>

<template>
  <div>
    <h3>{{ pokemon.name }}</h3>
    <p>{{ pokemon.types.join("") }}</p>
    <img :src="pokemon.image" width="100">
  </div>
</template>

完成!

ピカチュウを検索してみます。

GraphQLを使用する上での注意点

詳細はGraphQLを導入する時に考えておいたほうが良いことに、GraphQLの恩恵を受けるために考慮すべき点とその対策が記載されています。

上記の記事で紹介されている課題のうちいくつか例をあげると、

  • データを取得する際、N+1問題が起こる場合がある
  • クエリが複雑になるとリクエストボディが肥大化する
  • POSTを利用するためHTTPキャッシュに乗らない
    などがあります。

まとめ

GraphQLを利用する際は、関連する必要なデータを1回で取得するクエリを作り、それをPOSTします。
RESTFul APIに比べて、API呼び出しの回数が削減され、サービスのパフォーマンスの向上が期待できます。

一方で、愚直な実装をすると十分にGraphQLの恩恵を受けられないことがあるため、今後も勉強していきたいですね。

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?