虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

RustでGraphQLやってみる番外編(Vue+composition APIでGraphQLを使う)

皆さんこんにちは。とらラボのY.Fです。

前回までの記事ではRust+actix-web+juniperでGraphQLサーバーを作ってきました。
前回の記事の段階で一旦参照、更新処理はできたので、今回は番外編として呼び出し側となるクライアント側について記事を書いていこうと思います。

(前回の記事はこちら) toranoana-lab.hatenablog.com

この記事ではcomposition APIに関する細かい説明はしません。以前書いた記事があるので、そちらを御覧ください。
また、利用するライブラリがVue3にまだ対応していないので、下記記事で作ったプロジェクトをベースに作っていきます。

toranoana-lab.hatenablog.com

環境

サーバー側の環境は前回と同様なので割愛します。
React+GraphQLのサンプルは世にあふれているので、今回はVue+composition APIを使ってみようと思います。そのために必要な環境を列挙しておきます。
また、Vue周りの環境も基本は上記記事と同様なので割愛します。今回はVueでGraphQLを使うために必要なものだけ紹介します。

  • apollo 2.28.3
    • 公式サイトではGraphQL実装の表示と謳ってますが、ライブラリと思っておいて貰えれば良いです
  • vue-cli-plugin-apollo 0.22.2
    • apollo用のvue cliプラグインです
  • @vue/apollo-composable 4.0.0-alpha.6
    • vue-apolloをcomposition apiで使うためのライブラリです
    • 最新はalpha8ですが組み合わせの都合でalpha6を使います
  • eslint-plugin-graphql
    • バージョンはお好みでOKです
    • *.graphqlファイルのチェックなどをしてくれるeslintプラグインです

準備

必要なものをインストールしていきます。

$ npm install -g apollo

次に、vue-cli用のapolloプラグインを追加します。

$ vue add apollo

前回記事で作ったGraphQLサーバーから schema.json などを生成します。
GraphQLサーバーを起動しておく必要があるので前回作ったサーバーを起動しておいてください。
プロジェクトディレクトリでコマンド実行すると色々とバッティングするので、適当な場所でコマンドを実行します。(今回はデスクトップにしました。)

$ apollo client:download-schema --endpoint=http://localhost:3000/graphql schema.graphql
$ apollo client:download-schema --endpoint=http://localhost:3000/graphql

このコマンドを実行すると、 schema.graphql および、 schame.json が生成されます。各種ファイルをプロジェクトディレクトリの src/graphql ディレクトリに置くことにします。 中身はこんな感じで、GraphQLのスキーマ定義をAPIから逆引きしている感じになってます。(jsonファイルは大きいので割愛します。)

(schema.graphql)

"""
Direct the client to resolve this field locally, either from the cache or local resolvers.
"""
directive @client(
  """
  When true, the client will never use the cache for this value. See
  https://www.apollographql.com/docs/react/essentials/local-state/#forcing-resolvers-with-clientalways-true
  """
  always: Boolean
) on FIELD | FRAGMENT_DEFINITION | INLINE_FRAGMENT

"""
Export this locally resolved field as a variable to be used in the remainder of this query. See
https://www.apollographql.com/docs/react/essentials/local-state/#using-client-fields-as-variables
"""
directive @export(
  """The variable name to export this field as."""
  as: String!
) on FIELD

"""
Specify a custom store key for this result. See
https://www.apollographql.com/docs/react/advanced/caching/#the-connection-directive
"""
directive @connection(
  """Specify the store key."""
  key: String!

  """
  An array of query argument names to include in the generated custom store key.
  """
  filter: [String!]
) on FIELD

"""GraphQLのミューテーション系(更新系)リゾルバ"""
type Mutation {
  createPhoto(newPhoto: NewPhoto!): Photo!
  updatePhoto(id: Int!, updatePhoto: UpdatePhoto!): Photo!
}

"""A Photo insert struct"""
input NewPhoto {
  name: String!
  description: String
}

"""実際にGraphQLとしての型になるのは以下アトリビュートがついているこちら"""
type Photo {
  id: ID!
  name: String!
  description: String!
  url: String!
}

"""GraphQLのクエリ系リゾルバ"""
type Query {
  allPhotos: [Photo!]!
}

"""A Photo update struct"""
input UpdatePhoto {
  name: String
  description: String
}

合わせて、 apollo.config.js ファイルを書き換えておきます。

(apollo.config.js)

const path = require("path");

// Load .env files
const { loadEnv } = require("vue-cli-plugin-apollo/utils/load-env");
const env = loadEnv([
  path.resolve(__dirname, ".env"),
  path.resolve(__dirname, ".env.local")
]);

module.exports = {
  lintGQL: true,
  client: {
    name: env.VUE_APP_APOLLO_ENGINE_CLIENT,
    includes: ["src/graphql/**/*.{js,jsx,ts,tsx,vue,gql}"],
    service: {
      url: "http://localhost:3000/graphql",
      name: env.VUE_APP_APOLLO_ENGINE_SERVICE,
      localSchemaFile: path.resolve(__dirname, "./src/graphql/schema.json")
    }
  }
};

次に、main.ts でapolloを使えるようにしてあげます。

import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
// @ts-ignore
import { createProvider } from "./vue-apollo.js";  // 追加
import VueCompositionApi, { provide } from "@vue/composition-api";  // 追加
import { DefaultApolloClient } from "@vue/apollo-composable";  // 追加

Vue.use(VueCompositionApi);
const apolloProvider = createProvider();  // 追加

// @ts-ignore
new Vue({
  render: h => h(App),

  // 追加
  // @ts-ignore
  setup: function() {
    provide(DefaultApolloClient, apolloProvider.defaultClient);
  }
}).$mount("#app");

ところどころ @ts-ignore がついているのは、*.jsファイルを読み込んでたりする都合です。
anyの利用を許していたりする場合は不要になるかと思います。これもVue3に対応すれば不要になるとは思いますが、過渡期ゆえの対応になります。

最後に、自動生成されている vue-apollo.js ファイルの中身を少し変えます。
主にデフォルトのGraphQLのエンドポイントURLが異なるための処置になります。なので、 localhost:4000 となってる場所を localhost:300 などに変更して貰えればOKです。GraphQLサーバーを4000ポートで起動している場合などは不要です。
また、今回はWebSocketを使用しないので wsEndpoint のデフォルト値をnullにしておきます。

ここまで行ったら他のライブラリをインストール後、 npm run serve で起動できるはずです。

VueからQueryを実行する

では、早速GraphQLのQueryを実行できるようにしてみたいと思います。composition APIを組み合わせて使うには、以下が参考になります。

v4.apollo.vuejs.org

まずはじめに、疎通部分を*.vueのコンポーネントファイルにベタ書きするのではなく、GraphQLとの接続部分をファイルに切り出すことにします。
src/graphql フォルダ以下にファイルを作って以下のような感じで記述します。

(src/graphql/query.ts)

import { useQuery } from "@vue/apollo-composable";
import gql from "graphql-tag";

export const queryAllPhotos = () =>
  useQuery(gql`
    query allPhotos {
      allPhotos {
        id
        name
        url
        description
      }
    }
 `);

composition APIを使うにあたって、それらしく書ける方法を @vue/apollo-composable が提供してくれます。
Query系に関しては useQuery がそれに当たります。

このファイルを作った状態で、型定義ファイルを生成するapolloのコマンドを実行します。

$ apollo client:codegen --target=typescript --outputFlat src/graphql/types

実行後、src/graphql/types にファイルが生成されているのが確認できるかと思います。

(src/graphql/types/allPhotos.ts)

/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.

// ====================================================
// GraphQL query operation: allPhotos
// ====================================================

export interface allPhotos_allPhotos {
  __typename: "Photo";
  id: string;
  name: string;
  url: string;
  description: string;
}

export interface allPhotos {
  allPhotos: allPhotos_allPhotos[];
}

allPhotos_allPhotos という名前が不格好ですが、直接使うことはまず無いと思うので良しとしておきます。
これで、GraphQLのQueryに対する型定義ができたので、上記に書いた query.ts を方を使う形で編集します。

import { useQuery } from "@vue/apollo-composable";
import gql from "graphql-tag";
import { allPhotos } from "./types/allPhotos";

// 名前がバッティングするので変えました
export const queryAllPhotos = () =>
  // ジェネリクスを使うことで戻り値の型を決定できます。
  useQuery<allPhotos>(gql`
    query allPhotos {
      allPhotos {
        id
        name
        url
        description
      }
    }
  `);

では、App.vue から呼び出してみましょう

<template>
  <div>
    <p v-for="(photo, idx) of photos" :key="idx">
      <span>{{ photo.id }}</span>
      <span>{{ photo.name }}</span>
      <span>{{ photo.url }}</span>
      <span>{{ photo.description }}</span>
    </p>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";
import { queryAllPhotos } from "./graphql/query";
import { useResult } from "@vue/apollo-composable";

export default defineComponent({
  name: "App",
  setup() {
    const { result } = queryAllPhotos();
    const photos = useResult(result, null);
    return {
      photos
    };
  }
});
</script>

<style lang="scss"></style>

注: CORS設定で怒られる場合はサーバー側のCORSの設定が必要になります。こちらを参照ください。

docs.rs

f:id:toranoana-lab:20200617173247p:plain
実行結果

useResult で作られた変数が自動で reactive になってくれます。GraphQLで問い合わせた結果が自動でテンプレート側に反映されることになります。

VueからMutationを実行する

続いて更新系処理のMutationを実行してみたいと思います。

Queryと同様に src/graphql 以下に mutation.ts を作ります。

import gql from "graphql-tag";

export const CREATE_PHOTO = gql`
  mutation createPhoto {
    createPhoto(newPhoto: { name: "test", description: "test" }) {
      id
      name
      description
      url
    }
  }
`;

今回は簡単のため決まった値の挿入にしています。
mutationの発火自体は*.vueファイル内で行うので、ここではクエリ用の文字列だけを定義しておきます。

Queryと同様に型定義ファイルを生成します。

$ apollo client:codegen --target=typescript --outputFlat src/graphql/types

次に、App.vueにボタンとイベント付けます。

<template>
  <div>
    <button @click="addPhotoEvent">Photo追加</button>
    <p v-for="(photo, idx) of photos" :key="idx">
      <span>{{ photo.id }}</span>
      <span>{{ photo.name }}</span>
      <span>{{ photo.url }}</span>
      <span>{{ photo.description }}</span>
    </p>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";
import { queryAllPhotos } from "./graphql/query";
import { CREATE_PHOTO } from "./graphql/mutation";
import { useResult, useMutation } from "@vue/apollo-composable";

export default defineComponent({
  name: "App",
  setup() {
    // mutation後更新のためrefetch追加
    const { result, refetch } = queryAllPhotos();
    const { mutate: createPhoto } = useMutation(CREATE_PHOTO, () => ({
      variables: {},
      update: () => {
        // 更新後にrefetchを発火してデータを全部取り直す
        refetch();
      }
    }));
    const photos = useResult(result, null);

    const addPhotoEvent = async () => {
      await createPhoto();
    };
    return {
      photos,
      addPhotoEvent
    };
  }
});
</script>

<style lang="scss"></style>

ボタンを押すたびに追加されていくようになりました。

f:id:toranoana-lab:20200617180147g:plain
mutation動作

まとめ

今回はいつものサーバー側GraphQLの話から角度を変えてフロント側について書いてみました。
composition APIと合わせて使うことで、ソース上はただの関数呼び出しであるかのように使えて、実際のAPI通信はGraphQLのスキーマだけ意識しておけば良くなるのは大きなメリットだと思います。
ただ、今回は現時点(2020/06/19)でベータ版がリリースされているVue3の利用にはしませんでした。これは、Vue3にしてしまうと一部ライブラリが使えなくなってしまうためです。 アップデートの過渡期なので仕方ないですが、情報収集は気をつける必要があります。
早くVue3対応してもらえれば、余計なライブラリが不要になるので今後に期待です。

P.S

虎の穴ラボでの開発に少しでも興味を持っていただけた方は、採用説明会やカジュアル面談という場でもっと深くお話しすることもできます。ぜひお気軽に申し込みいただければ幸いです。
カジュアル面談では虎の穴ラボのエンジニアが、開発プロセスの内容であったり、「今期何見ました?」といったオタクトークから業務の話まで何でもお応えします。

カジュアル面談や採用情報はこちらをご確認ください。
yumenosora.co.jp

また、毎週火曜、木曜にはTora-Lab Meetup!と称して虎の穴ラボのエンジニア・採用担当とお話できる機会を設けさせていただくことになりました。
虎の穴ラボに興味がある、エンジニアや採用担当に質問したいことがある、などどなたでもご参加下さい。
news.toranoana.jp

さらに、弊社では新型コロナウイルス感染症終息後もフルリモートを継続導入することになりました!
地方在住のまま働きたい人など、上記Meetupやカジュアル面談、面接すべてリモート対応していますので、ご興味のある方はぜひいずれか応募してみてください! prtimes.jp

イベント情報

6月24日に「【オンライン開催】フリーテーマLT【とらのあなLT】」を開催します。
どなたでも参加できますのでご興味のある方はぜひお申し込みください! yumenosora.connpass.com

7月10日には定例開催している会社説明会をオンライン開催します。 どなたでも参加できるので、とらラボがどんなところか聞いてみたいという人は是非参加してください。 yumenosora.connpass.com