graphql-codegen + @vue/apollo-composable で型安全な GraphQL アクセスを試してみる
はじめに
自分の中で
「Apollo
って React
用でしょ? Vue
で使うと余計なモジュールも入るんでしょ?」
みたいな認識があって今まで避けてきていたんですが、その他の方法だと型安全にならなかったり、フレキシブルさが無くなったり、何なら結局モジュールの内部的には Apollo
を使っていたりしたので、Apollo
をちゃんと使ってみる事にしました。
今まで graphql-codegen
を使ってはいたのですが、Vue
の Apollo
用クライアントは使った事が無く、今回は表題の通りgraphql-codegen
+ @vue/apollo-composable
の構成でやってみたいと思います。
基本的には公式サイトの通りに進めていくだけですが、途中で少しハマりポイントがあったので自分用にまとめてみます。
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
のクエリに対しても補完が効くようになります。
インストール後、 .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'
}
)
動作としては、
- スキーマから基本的な型情報を等を取得
-
.vue
や.ts
から実際に使用されている箇所を抜き出す -
ts
の型情報をファイル(この場合@/gql
)に書き出し - 使用箇所で型情報が使えるようになる
- (
--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()
を使うと良いようです。
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