0
2

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 3 years have passed since last update.

さくっとNuxt.jsとApolloでGraphQLの雰囲気を掴むハンズオン

Posted at

本記事の目的

Nuxt.jsとApollo Clientを用いたハンズオンを通してGraphQLの雰囲気を掴むことを目的としています。

バックエンドにはpokemon APIを活用します。

最終的には以下のようなポケモン図鑑が完成します。
(なおDeploy作業までは扱っておりません。)

https://nuxt-graphql-demo.netlify.app/

開発環境

  • macOS
  • node v14.15.5

GraphQLとは

  • API向けのクエリ言語
  • 特徴として以下の点があります。
    • RESTでは複数のendipointが存在し、用途に応じてrequestを送り不必要なデータも含むresponseがあった反面、GraphQLでは単一のendpointに対して、欲しい情報を指定してrequest、responseを得ることができる。
    • request時に必要なresponseを指定することでフロントエンドでの開発が行いやすくなる

開発開始

プロジェクト作成 

npx create nuxt-appコマンドにてプロジェクトを作成します。


npx create-nuxt-app nuxt-graphql-example

対話形式でNuxt.jsのアプリを立ち上げることができます。
今回は以下のようにオプションを選択します。

Programming languageはTypescript(以下TSと略)を選択していますが、当アプリで型を用いるメリットはほぼないため、Javascriptで開発を進めていきます。

また、UI構築を簡単にするためVuetifyを用います。結果的に工数をかなり減らすことができました。


? Programming language: TypeScript
? Package manager: Yarn
? UI framework: Vuetify.js
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git

現時点でyarn devにてローカルサーバの立ち上げを確認できます。

Screenshot 2021-05-03 at 14.02.50.png

Apollo Clientセットアップ

GraphQLをもちいたrequestを送るためにApollo Clientを用います。

ライブラリをインストール


yarn add @nuxtjs/apollo graphql-tag

clientを読み込む設定をnuxt.config.jsに行っていきます。


// nuxt.config.js

modules: [
    '@nuxtjs/apollo',
  ],

  // Apollo module configuration
  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint: 'http://localhost:4000',
      }
    }
  },

TS対応のためgql.d.tsを新規作成し、記述していきます。


// gql.d.ts

declare module '*.gql' {
  import { DocumentNode } from 'graphql'

  const content: DocumentNode
  export default content
}

declare module '*.graphql' {
  import { DocumentNode } from 'graphql'

  const content: DocumentNode
  export default content
}

実行するクエリを用意

プロジェクト配下にapollo/queriesディレクトリを新規作成し、クエリを用意します。


├── apollo
│   └── queries
│       ├── pokemon.gql
│       └── pokemons.gql

  • pokemon.gql
    • idを引数に該当するpokemonの情報を取得
    • $id:String!でString型で必ず引数を受け取る(Non-nullable)

# pokemon.gql

query pokemon($id: String!) {
  pokemon(id: $id) {
    name
    classification
    types
    resistant
    weaknesses
    evolutions {
      name
      id
      image
    }
    evolutionRequirements {
      name
      amount
    }
    image
  }
}


  • pokemons.gql
    • $amount:Int!で取得するpokemonの種別数を必ずInt型で受け取る(Non-nullable)

# pokemons.gql 

query pokemons($amount: Int!) {
  pokemons(first: $amount) {
    id
    name
    image
  }
}


UI構築(pages)

pages配下にページネーション用のファイルを用意します。


├── pages
│   ├── index.vue
│   ├── pokemon
│   │   └── _id.vue
│   └── pokemons.vue


  • index.vue
    • トップページ
  • pokemons.vue
    • ポケモン一覧ページ
  • pokemon/_id.vue
    • ポケモン詳細ページ

index.vue

サイトの説明ページです。
好みに合わせてカスタマイズしましょう。


<template>
  <v-container fill-height>
    <v-row justify="center" align-content="center" class="">
      <v-col cols="12" align-self="auto">
          <h1 class="text-center">This is the demosite by using Nuxt.js + GraphQL(Apollo)</h1>
      </v-col>
      <v-col cols="12" align-self="auto"> 
          <h2 class="text-center">You can play the pokemomn picture book from sidebar menu</h2>
      </v-col>
    </v-row>
  </v-container>
</template>



Screenshot 2021-05-03 at 15.27.11.png

pokemons.gql

ポケモン一覧ページ

<script>内でapolloobjectを用いることでgraphQLのrequestを行うことができます。

respponseはapolloObject内で宣言したpokemonsを値として使うことができます。


<template>
  <v-container fluid>
    <v-row dense>
      <v-col v-for="pokemon in pokemons" :key="pokemon.id" :cols="12">
        <v-card>
          <NuxtLink :to="`pokemon/${pokemon.id}`">
            <v-img
              :src="pokemon.image"
              class="white--text align-end"
              gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"
              contain=true
              height="800"
            >
              <v-card-title v-text="pokemon.name"></v-card-title>
            </v-img>
          </NuxtLink>

          <!-- <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn icon>
              <v-icon>mdi-heart</v-icon>
            </v-btn>

            <v-btn icon>
              <v-icon>mdi-bookmark</v-icon>
            </v-btn>

            <v-btn icon>
              <v-icon>mdi-share-variant</v-icon>
            </v-btn>
          </v-card-actions> -->
        </v-card>
      </v-col>
    </v-row>
    <br>
    <v-expansion-panels accordion>
      <v-expansion-panel>
        <v-expansion-panel-header>Show Query Result</v-expansion-panel-header>
        <v-expansion-panel-content>
          {{ pokemons }}
        </v-expansion-panel-content>
      </v-expansion-panel>
    </v-expansion-panels>
  </v-container>
</template>
<script>
import "vue-apollo";
import pokemons from "~/apollo/queries/pokemons.gql";

// pokemonは最大151匹
let numGetPokemons = 151;

export default {
  data() {
    return {
      pokemons
    };
  },
  // pokemon一覧を取得
  apollo: {
    pokemons: {
      prefetch: "loading",
      query: pokemons,
      variables: {
        amount: numGetPokemons
      }
    }
  },
};
</script>

Screenshot 2021-05-03 at 15.34.27.png

pokemon/_id.vue

ポケモン一覧ページのNuxtLinkを用いて動的にポケモン詳細ページを生成します。

レンダリング時にresponseのpokemonが取得しきれずエラーになるため、v-if="pokemon"にて取得が完了でき次第ページを表示するようにします。

prefetch: ({ route }) => ({ id: route.params.id })variables() {return { id: this.$route.params.id };}を用いることでpath内のポケモンidを取得して、pokemonクエリの変数として用います。

※ 使用するコンポーネントsingleExplanation,multiExplanation,evolutionExplanationについては次章にて解説していきます。


<template>
  <div v-if="pokemon">
    <v-container fluid>
      <v-img
        :src="pokemon.image"
        class="white--text align-end"
        gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"
        contain=true
        height="800"
      >
        <v-card-title v-text="pokemon.name"></v-card-title>
      </v-img>
      <v-expansion-panels focusable>
        <!-- classification -->
        <single-explanation
          referKey="Classification"
          :referValue="pokemon.classification"
        />
        <!-- types -->
        <multi-explanation referKey="Types" :referValue="pokemon.types" />
        <!-- resistant -->
        <multi-explanation
          referKey="Resistant"
          :referValue="pokemon.resistant"
        />
        <!-- weaknesses -->
        <multi-explanation
          referKey="Weaknesses"
          :referValue="pokemon.weaknesses"
        />
        <!-- evolutions -->
        <evolution-explanation
          v-if="pokemon.evolutions"
          referKey="Evolutions"
          :referValue="pokemon.evolutions"
        />
        <!-- evolutionRequirements -->
        <multi-explanation
          v-if="pokemon.evolutionRequirements"
          referKey="EvolutionRequirements"
          :referValue="pokemon.evolutionRequirements"
        />

        <!-- レスポンス -->
        <v-expansion-panel>
          <v-expansion-panel-header>Show Query Result</v-expansion-panel-header>
          <v-expansion-panel-content class="justify-center">
            {{ pokemon }}
          </v-expansion-panel-content>
        </v-expansion-panel>
      </v-expansion-panels>
    </v-container>
  </div>
</template>

<script>
import pokemon from "~/apollo/queries/pokemon.gql";
import multiExplanation from "~/components/multiExplanation.vue";
import singleExplanation from "~/components/singleExplanation.vue";
import evolutionExplanation from "~/components/evolutionExplanation.vue";

export default {
  apollo: {
    pokemon: {
      query: pokemon,
      prefetch: ({ route }) => ({ id: route.params.id }),
      variables() {
        return { id: this.$route.params.id };
      }
    }
  },
  components: {
    multiExplanation,
    singleExplanation,
    evolutionExplanation
  }
};
</script>

UI構築(components)

responseに対応してUIを表示するcomponentsを作成します。

singleExplanation,multiExplanation,evolutionExplanationコンポーネントを作成していきます。


├── components
│   ├── evolutionExplanation.vue
│   ├── multiExplanation.vue
│   └── singleExplanation.vue

singleExplanation


<template>
    <v-expansion-panel>
      <v-expansion-panel-header>{{ referKey }}</v-expansion-panel-header>
      <v-expansion-panel-content class="text-center">
        {{ referValue }}
      </v-expansion-panel-content>
    </v-expansion-panel>
</template>
<script>
export default {
  props: ["referKey", "referValue"]
};
</script>


multiExplanation


<template>
  <v-expansion-panel>
    <v-expansion-panel-header>{{ referKey }}</v-expansion-panel-header>
    <v-expansion-panel-content v-for="(value, key) in referValue" :key="key" class="text-center">
      {{ value }}
    </v-expansion-panel-content>
  </v-expansion-panel>
</template>
<script>
export default {
  props: ["referKey", "referValue"]
};
</script>

evolutionExplanation

NuxtLinkを用いて進化するポケモンへのリンクを貼ります。


<template>
  <v-expansion-panel>
    <v-expansion-panel-header>{{referKey}}</v-expansion-panel-header>
    <v-expansion-panel-content v-for="(value, key) in referValue" :key="key" class="text-center">
      <NuxtLink :to="`${value.id}`">
      {{value.name}}
      </NuxtLink>
    </v-expansion-panel-content>
  </v-expansion-panel>
</template>
<script>
export default {
  props:[
    'referKey',
    'referValue'
    ]
};
</script>

Screenshot 2021-05-03 at 15.53.13.png

UI構築(その他)

お好みでdefault.vueを修正してSidebarの項目やlayoutを整えます


├── layouts
│   ├── default.vue
│   └── error.vue


<template>
  <v-app dark>
    <v-navigation-drawer
      v-model="drawer"
      :mini-variant="miniVariant"
      :clipped="clipped"
      fixed
      app
    >
      <v-list>
        <v-list-item
          v-for="(item, i) in items"
          :key="i"
          :to="item.to"
          :href="item.href"
          router
          exact
        >
          <v-list-item-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title v-text="item.title" />
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>
    <v-app-bar
      :clipped-left="clipped"
      fixed
      app
    >
      <v-app-bar-nav-icon @click.stop="drawer = !drawer" />
      <v-toolbar-title v-text="title" />
      <v-spacer />
    </v-app-bar>
    <v-main>
      <v-container>
        <nuxt />
      </v-container>
    </v-main>
    <v-footer
      :absolute="!fixed"
      app
    >
      <span>&copy; {{ new Date().getFullYear() }}</span>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data () {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      items: [
        {
          icon: 'mdi-apps',
          title: 'Welcome',
          to: '/'
        },
        {
          icon: 'mdi-format-list-bulleted',
          title: 'Pokemons',
          to: '/pokemons'
        },
        {
          icon: 'mdi-code-tags',
          title: ' Github',
          href: 'https://github.com/kimkiyong0612/Nuxt-GraphQL-Demo'
        }
      ],
      miniVariant: false,
      right: true,
      rightDrawer: false,
      title: 'Nuxt.js GraphQL(Pockemon API) Demo'
    }
  }
}
</script>


結び

以上をもってポケモン図鑑を作成することができました。

あとはherokuNetlifyにデプロイすることで0円でサイトを公開することができます。
(VercelではSSRのDeployがうまくいかなかったため、成功した方がいたら教えてもらえるとうれしいです。)

おつかれさまでした。

Reference

0
2
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
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?