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を使います。
<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>
上手くいったら、↓のようにタイトルのリストが表示されます。
ただし、このコードには残念な点もあります。 fetch
はブラウザのAPIなので、SSR時にデータを取得しようとするとエラーになります( created
=> asyncData
に変更すると、SSR時のエラーを再現できます)。
この問題の解決策としては node-fetch を使うといった方針もありですが、ここではNuxtのapollo-moduleを使って解決してみます。
apollo-moduleのインストール
まず、公式のインストールガイドに沿って、apollo-moduleをインストールします。
yarn add @nuxtjs/apollo graphql-tag
nuxtの設定ファイルにモジュールを読み込み、最低限の設定を記述します。
modules: ['@nuxtjs/apollo'],
apollo: {
clientConfigs: {
default: {
httpEndpoint:
'https://swapi-graphql.netlify.com/.netlify/functions/index'
}
}
},
この状態で、pages/index.vueのscriptを以下のように書き換えると、Apolloを使ってデータが取得できるようになります!
<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()
を使って型をつけようとすると、コンパイルエラーが発生します。
<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
の型を直せば、コンパイルが通るようになります。
<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
次に設定ファイル。
overwrite: true
schema: "schema.graphql"
generates:
lib/GraphQL/generated.ts:
plugins:
- "typescript"
準備ができたらコマンドを叩きます。
yarn run graphql-codegen --config codegen.yml
上手くいけば、以下のようなファイルがができているはず。
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して使うだけです。先ほどと少し型定義が変わってるので、それに合わせてコードも直してます。
<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のレスポンスに型がつけられるようになりました。