LoginSignup
24
16

More than 3 years have passed since last update.

Nuxt.js + apollo-module(vue-apollo) + TypeScriptで型をつける方法

Last updated at Posted at 2020-03-26

Vue.jsでGraphQLを使う際は、Vue Apolloが選択肢の一つになると思います。

GraphQLのクライアントはfetchやXMLHttpRequestを使用しても実装できます。しかし、Nuxt.jsでSSRすることを考えると、サーバサイドでも取得できるようにしたり、Vueのdataとマージしたりと、色々と考慮すべきことや実装すべきポイントがあります。Vue Apolloを使うと、こういった部分であまり悩まなくて済みます。

※ただし、Apolloは高機能である分、使いこなすまで時間がかかりますし、ユースケースによってはオーバースペックな可能性もあります。

Vue ApolloはVue.jsのプラグインで、ApolloのGraphQLクライアントをVue.jsに統合することができます。

さらに、Nuxt.jsのapollo-moduleは、Vue ApolloをNuxt.jsに統合しています。

この記事では、Nuxt.js + TypeScript + apollo-moduleで開発する際に、型をつける方法を解説します。

なお、この記事で使っているソフトウェアのバージョンは下記の通りです。

  • nuxt: 2.12.0
    • vue: 2.6.11
  • typescript: 3.8.3
  • nuxtjs/apollo: 4.0.0-rc19
    • vue-apollo: 3.0.3

また、本記事のサンプルコードは下記リポジトリでも公開しています。

セットアップ

下記コマンドでNuxtプロジェクトを作成します。

npx create-nuxt-app nuxt-apollo-typescript

セットアップ中の設定は、プログラミング言語がTypeScript、レンダリングモードがUniversal (SSR)になっていれば、他は何でもよいです。

fetchによるGraphQLクライアント

まずはシンプルなところから始めたいので、fetchを使った最小限のGraphQLクライアントを作ってみます。GraphQL APIはサンプルとしてよく使われるSW APIを使います。

pages/index.vue
<template>
  <div :class="$style.container">
    <h1>Star wars films</h1>
    <ul>
      <li v-for="film in films" :key="film.episodeID">
        {{ film.title }}
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

// GraphQLのレスポンスに型をつける
interface Film {
  episodeID: number
  title: string
}
interface Edge {
  node: Film
}
interface FilmConnection {
  edges: Edge[]
}
interface ResponseData {
  allFilms: FilmConnection
}
interface Response {
  data: ResponseData
}

// Vueのdataに型をつける
interface VueData {
  films: Film[]
}

export default Vue.extend({
  data(): VueData {
    return {
      films: []
    }
  },
  async created() {
    const query = `
{
  allFilms(first: 3) {
    edges {
      node {
        episodeID
        title
      }
    }
  }
}
`
    const res = await fetch(
      'https://swapi-graphql.netlify.com/.netlify/functions/index',
      {
        method: 'POST',
        body: JSON.stringify({
          query
        }),
        headers: {
          'Content-Type': 'application/json'
        }
      }
    ).then<Response>((res) => res.json())

    this.films = res.data.allFilms.edges.map((e) => e.node)
  }
})
</script>

<style module>
.container {
  margin: 10px;
}
</style>

上手くいったら、↓のようにタイトルのリストが表示されます。

Screen Shot 2020-03-25 at 1.45.08.png

ただし、このコードには残念な点もあります。 fetch はブラウザのAPIなので、SSR時にデータを取得しようとするとエラーになります( created => asyncData に変更すると、SSR時のエラーを再現できます)。

この問題の解決策としては node-fetch を使うといった方針もありですが、ここではNuxtのapollo-moduleを使って解決してみます。

apollo-moduleのインストール

まず、公式のインストールガイドに沿って、apollo-moduleをインストールします。

yarn add @nuxtjs/apollo graphql-tag

nuxtの設定ファイルにモジュールを読み込み、最低限の設定を記述します。

nuxt.config.js
  modules: ['@nuxtjs/apollo'],
  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint:
          'https://swapi-graphql.netlify.com/.netlify/functions/index'
      }
    }
  },

この状態で、pages/index.vueのscriptを以下のように書き換えると、Apolloを使ってデータが取得できるようになります!

pages/indev.vue
<script>
import gql from 'graphql-tag'

export default {
  computed: {
    films() {
      return this.allFilms.edges.map((e) => e.node)
    }
  },
  apollo: {
    allFilms: gql`
      query {
        allFilms(first: 3) {
          edges {
            node {
              episodeID
              title
            }
          }
        }
      }
    `
  }
}
</script>

Vueコンポーネントの定義にapolloというプロパティを生やして、ここにGraphQL APIに対するクエリを書くと、レスポンスがdataにマージされます。

TypeScriptの型定義

現状では、apolloによるデータ取得はできましたが、TypeScriptの型は付いていません。そこで、 lang="ts" にして、 Vue.extend() を使って型をつけようとすると、コンパイルエラーが発生します。

pages/index.vue
<script lang="ts">
import Vue from 'vue'
import gql from 'graphql-tag'

export default Vue.extend({
  computed: {
    films() {
      return this.allFilms.edges.map((e) => e.node)
    }
  },
  apollo: {
    allFilms: gql`
      query {
        allFilms(first: 3) {
          edges {
            node {
              episodeID
              title
            }
          }
        }
      }
    `
  }
})
</script>

コンパイルエラー:

No overload matches this call.
  The last overload gave the following error.
    Argument of type '{ computed: { films(): any; }; apollo: { allFilms: DocumentNode; }; }' is not assignable to parameter of type 'ComponentOptions<Vue, DefaultData<Vue>, DefaultMethods<Vue>, DefaultComputed, PropsDefinition<Record<string, any>>, Record<...>>'.
      Object literal may only specify known properties, and 'apollo' does not exist in type 'ComponentOptions<Vue, DefaultData<Vue>, DefaultMethods<Vue>, DefaultComputed, PropsDefinition<Record<string, any>>, Record<...>>'.

これは、VueのOptions APIにapolloというフィールドが存在しないために発生しています。そこで、tsconfig.jsonを修正し、VueのOptions APIでapolloが定義できるよう修正しましょう。

  "compilerOptions": {
    "types": [
      "@types/node",
      "@nuxt/types",
+     "vue-apollo"
    ]
  }

これで、TypeScriptのコンパイル時にVue Apolloが定義しているapolloの型定義が読み込まれるため、apolloの型がつくようになります。

あとは this.allFilms の型を直せば、コンパイルが通るようになります。

pages/index.vue
<script lang="ts">
import Vue from 'vue'
import gql from 'graphql-tag'

// GraphQLのレスポンスに型をつける
interface Film {
  episodeID: number
  title: string
}
interface Edge {
  node: Film
}
interface FilmConnection {
  edges: Edge[]
}

// Vueのdataに型をつける
interface Data {
  allFilms: FilmConnection
}

export default Vue.extend({
  data(): Data {
    return {
      allFilms: {
        edges: []
      }
    }
  },
  computed: {
    films(): Film[] {
      return this.allFilms.edges.map((e) => e.node)
    }
  },
...

レスポンスの型をスキーマから生成する

現状では、 FilmConnection のような型を手書きしていますが、こういった型はGraphQLのスキーマから自動生成したいところ。そこで、まずは以下のURLからスキーマをもってきます。

これを schema.graphql といった名前でプロジェクト内に置いておきます。次に、GraphQL Code Generatorを使って、このスキーマからTypeScriptの型を生成します。

まずはライブラリのインストール

yarn add -D @graphql-codegen/cli @graphql-codegen/typescript

次に設定ファイル。

codegen.yml
overwrite: true
schema: "schema.graphql"
generates:
  lib/GraphQL/generated.ts:
    plugins:
      - "typescript"

準備ができたらコマンドを叩きます。

yarn run graphql-codegen --config codegen.yml

上手くいけば、以下のようなファイルがができているはず。

lib/GraphQL/generated.ts
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};
...

あとは、これをimportして使うだけです。先ほどと少し型定義が変わってるので、それに合わせてコードも直してます。

pages/index.vue
<script lang="ts">
import Vue from 'vue'
import gql from 'graphql-tag'
import { FilmsConnection, Film } from '~/lib/GraphQL/generated'

interface Data {
  allFilms?: FilmsConnection
}

export default Vue.extend({
  data(): Data {
    return {
      allFilms: undefined
    }
  },
  computed: {
    films(): Film[] {
      if (this.allFilms == null || this.allFilms.edges == null) return []
      return this.allFilms.edges
        .map((e) => e?.node)
        .filter((f): f is Film => f != null)
    }
  },
  apollo: {
    allFilms: gql`
      query {
        allFilms(first: 3) {
          edges {
            node {
              episodeID
              title
            }
          }
        }
      }
    `
  }
})
</script>

これによって、 lang="ts" なVueコンポーネントでapolloが使えるようになり、さらに、GraphQLのレスポンスに型がつけられるようになりました。

24
16
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
24
16