📑

graphql-codegen + @vue/apollo-composable で型安全な GraphQL アクセスを試してみる

2023/03/01に公開

はじめに

自分の中で

Apollo って React 用でしょ? Vue で使うと余計なモジュールも入るんでしょ?」

みたいな認識があって今まで避けてきていたんですが、その他の方法だと型安全にならなかったり、フレキシブルさが無くなったり、何なら結局モジュールの内部的には Apollo を使っていたりしたので、Apollo をちゃんと使ってみる事にしました。

今まで graphql-codegen を使ってはいたのですが、VueApollo 用クライアントは使った事が無く、今回は表題の通りgraphql-codegen + @vue/apollo-composable の構成でやってみたいと思います。

基本的には公式サイトの通りに進めていくだけですが、途中で少しハマりポイントがあったので自分用にまとめてみます。

https://the-guild.dev/graphql/codegen/docs/guides/react-vue

Vue プロジェクトを作成

まずはプロジェクトを作ります。

npm init @vue/latest

適宜必要な機能を追加します。
今回の記事通りに進める場合は少なくとも TypeScript を有効にする必要があります。

初期化が終わったら、作成されたプロジェクトディレクトリに降りてモジュールをインストールします。今回は yarn を使用します。

yarn install

graphql-codegen のインストール

公式サイトにあるように、以下のモジュールをインストールします。

yarn add graphql
yarn add -D typescript ts-node @graphql-codegen/cli @graphql-codegen/client-preset

codegen を実行するスクリプトを追加する

yarn であれば yarn graphql-codegen でも実行できるのですが、念のためパッケージスクリプトにも登録しておきます。

{
  ...
  "scripts": {
    ...
    "generate": "graphql-codegen --config codegen.ts"
    ...
  },
  ...
 }

@vue/apollo-composable をインストール

こちらもインストールします。

yarn add @apollo/client @vue/apollo-composable

codegen.ts を作成

schema.original にスキーマがあるとした時、以下のように codegen.ts をルートに作成します。

import type { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
  schema: 'schema.original/**.gql',
  documents: ['src/**/*.vue', 'src/**/*.ts', '!src/gql/**/*'],
  ignoreNoDocuments: true, // for better experience with the watcher
  generates: {
    './src/gql/': {
      preset: 'client',
      config: {
        useTypeImports: true
      }
    }
  }
}

export default config

VSCode で GraphQL コードの補完を効かせる

@vue/apollo-composable を使うと、.vue.ts ファイルの中に GraphQL のクエリを直接書く事になります。
以下の拡張機能を使うと、.vue.ts ファイル中の GraphQL のクエリに対しても補完が効くようになります。

https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql

インストール後、 .graphqlrc.yml を作成して以下の内容を書き込みます。

schema: 'schema.original/*.gql'
documents: 'src/**/*.{vue,graphql,js,ts,jsx,tsx}'

これで、補完が効くようになります。

ApolloClient の設定

useQuery 等で使用する ApolloClient を設定します。
今回のケースでは同じドメイン上の複数のエンドポイントを扱う想定で、それぞれ Bearer トークンでの認証になっていたため、data/apollo.ts というファイルを作成して以下のように設定しました。

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

const baseUrl = 'https://example.com/'

const fetchWithToken = (uri: RequestInfo, options: RequestInit) => {
  const token = localStorage.getItem('token')
  options.headers = {
    // トークンをセットする
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
  return fetch(uri, options)
}

const createClient = (path: string) => {
  return new ApolloClient({
    link: createHttpLink({
      uri: `${baseUrl}/${path}`,
      fetch: fetchWithToken
    }),
    cache: new InMemoryCache()
  })
}

export const apolloClients = {
  user: createClient('/api/user')
}

createClient("エンドポイントのパス")ApolloClient を作成できます。↑の例では、userというIDでクライアントを設定しました。

これを main.ts で呼び出してプロジェクト内で使えるようにします。

import { ApolloClients, provideApolloClients } from '@vue/apollo-composable'
import { createApp, h, provide } from 'vue'
import App from './App.vue'
import { apolloClients } from './data/apollo'

provideApolloClients(apolloClients)

const app = createApp({
  setup() {
    provide(ApolloClients, apolloClients)
  },
  render: () => h(App)
})

app.mount('#app')

setup() の中で provide しているものと provideApolloClients で設定しているものがありますが、僕のケースでは両方設定しないと動作しませんでした。(.vue だけでなく .ts からも呼び出しているから?)

graphql-codegen を監視付きで呼び出す

graphql-codegen には --watch というオプションがあり、これをつけて呼び出すと変更を監視して変更がある度に書き出し直してくれます。

コードを書き始める前に以下のように呼び出しておくと、書いているそばから型ファイルを生成してくれます。

yarn generate --watch

エラーが出る!

過去の記載

ただし、このまま実行すると以下ようなエラーが出ます。(2023/2/28現在)

TypeScriptLoader failed to compile TypeScript:
error TS5095: Option 'preserveValueImports' can only be used when 'module' is set to 'es2015' or later.

これは、デフォルトで生成される tsconfig.json にある extends が原因です。

{
  "extends": "@vue/tsconfig/tsconfig.web.json",
  ...
}

extends されたこのルールが原因で、いかに compilerOptions をいじろうとも動いてくれませんでした。

extends を削除するか、_extends 等名称変更すると書き出せるようになります。

ちなみに、 graphql-codegen を実行した後は extends の有無は問題にならないので、僕は書き出しが終わったら戻すようにしています。

クエリを書く

実際にコード内でクエリを書きます。ここでは、クライアントに先程作成した user を選択しています。

graphql() 内のコメント /* GraphQL */ は、拡張機能にこの記述が GraphQL である事を認識させるために記述しています。

import { useQuery } from '@vue/apollo-composable'
import { graphql } from '@/gql'

const { result } = useQuery(
  graphql(/* GraphQL */ `
    query getUserList($activeOnly: Boolean) {
      getUserList(activeOnly: $activeOnly) {
        id
	name
      }
    }
  `),
  { activeOnly: true },
  {
    clientId: 'user'
  }
)

動作としては、

  1. スキーマから基本的な型情報を等を取得
  2. .vue.ts から実際に使用されている箇所を抜き出す
  3. ts の型情報をファイル(この場合@/gql)に書き出し
  4. 使用箇所で型情報が使えるようになる
  5. (--watch をつけている場合は、変更があり次第2に戻る)

という形になっており、2 から 4 までに少し(といっても数秒)ラグがあります。

(おまけ) await を使う

useQuery を使った形だと、 Vue のテンプレートと連動してリアクティブに使いたい時には便利ですが、 await を使って処理を待ちたい場合に少し面倒な処理が必要になります。

import { useQuery } from '@vue/apollo-composable'
import { graphql } from '@/gql'

(async () => {
  const { result, onResult } = useQuery(
    graphql(/* GraphQL */ `
      query getUserList($activeOnly: Boolean) {
        getUserList(activeOnly: $activeOnly) {
          id
          name
        }
      }
    `),
    { activeOnly: true },
    {
      clientId: 'user'
    }
  )
  
  // Promise を作って結果が得られるまで await
  await new Promise<void>((res) => {
    const { off } = onResult(() => {
      off()
      res()
    })
  })

  console.log(result.value)
})()

調べたところ、こういうケースでは useApolloClient() を使うと良いようです。

https://github.com/vuejs/apollo/issues/1219

import { useApolloClient } from '@vue/apollo-composable'
import { graphql } from '@/gql'

const { resolveClient } = useApolloClient();

(async () => {
  const res = await resolveClient('user').query({
    query: graphql(/* GraphQL */ `
      query getUserList($activeOnly: Boolean) {
        getUserList(activeOnly: $activeOnly) {
          id
          name
        }
      }
    `),
    variables: {
      activeOnly: true
    }
  })

  console.log(res.data)
})()

おわりに

なんとなく忌避していた Apollo ですが、使ってみるととても便利でした。組み合わせなのか、エラーが出たり微妙に癖がありますが、アップデートでこの辺りも改善されていくといいですね。

Discussion