LoginSignup
12
8

More than 3 years have passed since last update.

GraphQLとVueを使ってミニアプリを作る

Last updated at Posted at 2020-10-23

はじめに

本記事ではVue Apolloを利用して、GraphQLとVueアプリを接続した後に、CRUD機能並びにSubscriptionの実装を目指す内容となります。

完成イメージ
スクリーンショット 2020-10-23 23.00.35.png

大まかな概要としては、Apolloサーバーから取得したデータをVueを利用して表示・クライアント側からGraphQLを操作することがゴールとなります。

記事内でGraphQL等の基本的な説明は省略しております。
もし宜しければ、GraphQLの基礎の基礎、並びにApolloサーバーとSubscriptionについて解説している前回の記事も合わせてご一読ください(Apolloサーバー部分は前回と同一のコードを利用します

Apolloサーバーを作る(前回の記事で既に作ってある方は省略)

先ずはApolloを使ってGraphQLのAPIサーバーを作ります!

プロジェクトを作った後に、npm経由でapollo-serverをインストールします。

$ mkdir graphql-server
$ cd graphql-server
$ mkdir resolver
$ touch index.js db.js 
$ resolver/{Query.js,Mutation.js,Subscription.js}
$ npm init
$ npm install apollo-server --save

データベース代わりのJavaScriptファイルを用意します。

db.js
const posts = [{
    id: '1',
    title: 'こころ',
    author: '夏目漱石'
}, {
    id: '2',
    title: '舞姫',
    author: '森鴎外'
}, {
    id: '3',
    title: '羅生門',
    author: '芥川龍之介'
}]

const db = {
    posts,
}

module.exports  = db;

続いてQuery,Mutation,そして今回のテーマであるSubscriptionのリゾルバ関数を書いたファイルを順番に用意していきます。

Query.js
const Query = {
    posts(parent, args, { db }, info) {
       //クエリを書いた時に引数が「ない」時
       //模擬データベースの内容を全て表示
        if (!args.query) {
            return db.posts
       //クエリを書いた時に引数が「ある」時
       //引数とtitle or authorが一致したものだけを表示
        }else{
            return db.posts.filter((post) => {
            const isTitleMatch = post.title.toLowerCase().includes(args.query.toLowerCase())
            const isAuthorMatch = post.author.toLowerCase().includes(args.query.toLowerCase())
            return isTitleMatch || isAuthorMatch
        })
    }
    }
}

module.exports  = Query

Queryのリゾルバ関数です。

Mutation.js
const Mutation = {
    createPost(parent, args, { db, pubsub }, info) {
        const postNumTotal = String(db.posts.length + 1)
        const post = {
            id: postNumTotal,
            ...args.data
        }

        //データベース更新
        db.posts.push(post)
        //サブスクリプション着火
        pubsub.publish('post', { 
                post: {
                    mutation: 'CREATED',
                    data: post
                }
             })
        return post
    },
    updatePost(parent, args, { db, pubsub }, info) {
        const { id, data } = args
        const post = db.posts.find((post) => post.id === id)
        if (!post) {
            throw new Error('Post not found')
        }

        if (typeof data.title === 'string' && typeof data.author === 'string') {
            //データベース更新
            post.title = data.title
            post.author = data.author
            console.log(post)
            //サブスクリプション着火
            pubsub.publish('post', {
            post: {
                mutation: 'UPDATED',
                data: post
            }
        })
        }

        return post
    },
    deletePost(parent, args, { db, pubsub }, info) {
        const post = db.posts.find((post) => post.id === args.id)
        const postIndex = db.posts.findIndex((post) => post.id === args.id)

        if (postIndex === -1) {
            throw new Error('Post not found')
        }
        //データベース更新
        db.posts.splice(postIndex, 1)
        //サブスクリプション着火
        pubsub.publish('post', {
                post: {
                    mutation: 'DELETED',
                    data: post
                }
            })
        return post
    },
}

module.exports  = Mutation

Mutationのリゾルバ関数では、データベースの更新とSubscriptionの着火をしています。

Subscription.js
const Subscription = {
    post: {
        subscribe(parent, args, { pubsub }, info) {
            return pubsub.asyncIterator('post')
        }
    }
}

module.exports = Subscription

Subscriptionのリゾルバ関数です。
pubsub.asyncIteratorでSubscriptionのイベントを非同期でリッスンします。

説明の関係で最後になりましたが、
スキーマの定義とサーバー起動のファイルになります。

index.js
const  {ApolloServer,PubSub,gql} = require('apollo-server');
const db = require('./db')
const Query = require('./resolver/Query')
const Mutation = require('./resolver/Mutation')
const Subscription = require('./resolver/Subscription')

//スキーマ定義
const typeDefs = gql`
type Query {
  posts(query: String): [Post!]!
}

type Mutation {
  createPost(data: CreatePostInput!): Post!
  deletePost(id: ID!): Post!
  updatePost(id: ID!, data: UpdatePostInput!): Post!
}

# Subscription
type Subscription {
  post: PostSubscriptionPayload!
}

input CreatePostInput {
  title: String!
  author: String!
}

input UpdatePostInput {
  title: String
  author: String!
}

type Post {
  id: ID!
  title: String!
  author: String!
}

######################
# Subscriptionで利用
######################

# enum型でMutation.js内のサブスクリプション着火と連動
enum MutationType {
  CREATED
  UPDATED
  DELETED
}

# Subscriptionのフィールド
type PostSubscriptionPayload {
  mutation: MutationType!
  data: Post!
}

`
//PubSubのインスタンスを作成,サブスクリプションが利用可能に!
const pubsub = new PubSub()

const server = new ApolloServer({
    typeDefs: typeDefs,
    resolvers: {
        Query,
        Mutation,
        Subscription,
    },
    context: {
        db,
        pubsub
    }
})

server.listen().then(({ url, subscriptionsUrl }) => {
    console.log(`🚀 Server ready at ${url}`);
    console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
  });

サーバーを立てる際にスキーマやリゾルバ、PubSubなどを引数に指定しています。
指定した引数はQueryやMutationそしてSubscription、それぞれの処理で利用をしています。

準備が出来たらターミナルから起動させます。

$ node index.js
🚀 Server ready at http://localhost:4000/
🚀 Subscriptions ready at ws://localhost:4000/graphql

こちらをGraphQLのエンドポイントとして利用します。
本記事ではサーバーが立ち上がっていないと、クライアント側からデータの表示や更新が出来ないので、必ずサーバーを立てることを忘れないようにしてください😊

Apolloクライアントの設定

Vueプロジェクトを作ろう

Vue CLIを使ってプロジェクトを作ります。

$ vue create apollo-client #2系を選択してVueプロジェクトの作成
$ cd apollo-client 
$ npm run serve # 起動

プロジェクトの作成に成功していた場合、下記のような画面になります。
http://localhost:8080/

スクリーンショット 2020-10-23 21.02.27.png

Vueプロジェクト内の設定

続いてVueでGraphQLを扱うために、Vue Apolloをインストールしていきます。
本記事においては設定は全てデフォルトで進みます(色々聞かれますが「No」と答えてます)

$ vue add apollo

見た目をリッチにするために、Vuetifyもインストールします。

$ vue add vuetify

以上にて、プロジェクトの設定は完了です。
Vue-ApolloとVuetifyの設定はmain.jsに記述があります。

main.js
import Vue from 'vue'
import App from './App.vue'
import { createProvider } from './vue-apollo'
import vuetify from './plugins/vuetify';

Vue.config.productionTip = false

new Vue({
  apolloProvider: createProvider(),
  vuetify,
  render: h => h(App)
}).$mount('#app')

importしているvue-apollo.jsではエンドポイントやログイン/ログアウトなどの設定が出来ます。

Queryの実装

QueryとMutationの実装はこちらの記事を参考にさせて頂きました。ありがとうございます!

模擬データベース内のデータを「読み取り」するGraphQLのクエリを作成します。

$ mkdir src/graphql
$ touch src/graphql/post-query.js

前回までの記事でlocalhost内で立ち上げたIDEに書いていたクエリがこちらの部分に該当します。importしているgqlについてはこちらを参照ください。

post-query.js
import gql from 'graphql-tag'

export const ALL_POSTS = gql`
query{
  posts{
    id
    title
    author
  }
}
`

続いてApp.vue内を書き換えます(説明の省略のため、こちらのファイルに書いていますが、component化した方が良いです!)

「apollo」のオプション内で別ファイルで定義したクエリを呼び出し、
dataプロパティで定義したpostsオブジェクトに格納します。

App.vue
<template>
  <v-app>
    <v-main>
        <v-container>
 <v-row
   style="width: 550px;"
>
    <!--ツールバー-->
    <v-toolbar  color="grey lighten-1">
      <v-toolbar-title>本棚</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn color="primary" dark class="mb-1">新規追加</v-btn>
    </v-toolbar>
    <!--本棚の中身-->
<div
v-for="post in posts" :key="post.id"
>
  <v-card
    class="mx-auto"
    width="550px"
    outlined
  >
    <v-list-item three-line>
      <v-list-item-content>
        <v-list-item-title class="headline mb-1">
           {{ post.title}}/{{ post.author}}
        </v-list-item-title>
        <v-list-item-subtitle>From Apollo-Server</v-list-item-subtitle>
      </v-list-item-content>
    </v-list-item>
  </v-card>
  </div>
  </v-row>
  </v-container>
    </v-main>
  </v-app>
</template>

<script>
//Query
import {ALL_POSTS} from "./graphql/post-query"

export default {
   name: "App",
    data: () => ({
      //本棚の中身を定義
      posts: [],
    }),
  apollo: {
    //本棚の中身
    posts: {
      //クエリを書いている部分
      query: ALL_POSTS,
        }
  },
 methods: {
    }
  }
</script>

ここまで書き終えたらブラウザで確認しましょう。

$ npm run serve

http://localhost:8080/

Vueを利用して模擬データベースの値を表示することが出来ました!

スクリーンショット 2020-10-23 22.31.34.png

Mutationの実装

続いてMutationの実装を行います。
こちらの章を終えると①書き込み、②更新、③削除 が出来るようになります。

先ずはファイルを作りましょう。

$ touch src/graphql/post-mutation.js

Mutationのクエリを書いていきます。

post-mutation.js
import gql from 'graphql-tag'

// POSTの新規追加
export const CREATE_POST = gql`mutation ($title: String!, $author: String!) {
    createPost(data: { title: $title, author: $author}) {
        id
         title
        author
     }
  }`

// POSTの更新
export const UPDATE_POST = gql`
    mutation updatePost($id: ID!, $title: String!, $author: String!) {
        updatePost(id:$id,data: {title: $title, author: $author}) {
            id
            title
            author
        }
    }
`

// // POSTの削除
export const DELETE_POST = gql`
    mutation deletePost($id: ID!) {
        deletePost(id:$id){
            title
            author
        }
    }
`

App.vueのmethod内に①書き込み、②更新、③削除の関数を作成。
updateQueryメソッドで上記3つを実行することが出来ます。

App.vue
<template>
  <v-app>
    <v-main>
        <v-container>
   <!--入力フォーム-->
    <v-dialog v-model="dialog" max-width="500px">
      <v-card>
        <v-container>
          <h2 v-if="isCreate">本棚に追加する</h2>
          <h2 v-if="!isCreate">本棚を更新する</h2>
          <v-form ref="form" v-model="valid" lazy-validation>
            <!--名前-->
            <v-text-field
                v-model="post.title"
                :rules="titleRules"
                :counter="20"
                label="タイトル"
                required
            ></v-text-field>
            <v-text-field
                v-model="post.author"
                :rules="authorRules"
                :counter="20"
                label="作者"
                required
            ></v-text-field>
            <!--追加ボタン-->
            <v-btn
                v-if="isCreate"
                :disabled="!valid"
                @click="createPost"
            >
              追加
            </v-btn>
            <!--更新ボタン-->
            <v-btn
                v-if="!isCreate"
                :disabled="!valid"
                @click="updatePost"
            >
              更新
            </v-btn>
            <v-btn @click="clear">クリア</v-btn>
          </v-form>
        </v-container>
      </v-card>
    </v-dialog>


          <v-row
   style="width: 550px;"
>
    <!--ツールバー-->
    <v-toolbar  color="grey lighten-1">
      <v-toolbar-title>本棚</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn color="primary" dark class="mb-1" @click="showDialogNew">新規追加</v-btn>
    </v-toolbar>

    <!--本棚の中身-->
<div
v-for="post in posts" :key="post.id"
>
  <v-card
    class="mx-auto"
    width="550px"
    outlined
  >
    <v-list-item three-line>
      <v-list-item-content>
        <v-list-item-title class="headline mb-1">
           {{ post.title}}/{{ post.author}}
        </v-list-item-title>
        <v-list-item-subtitle>From Apollo-Server</v-list-item-subtitle>
      </v-list-item-content>
    </v-list-item>
    <!-- 編集・削除ボタン -->
    <v-card-actions>
       <v-btn
              color="success"
              small
              @click="showDialogUpdate(post.id,post.title,post.author)"
          >
            <v-icon small>
              編集する
            </v-icon>
          </v-btn>
          <v-btn
              color="error"
              small
              @click="deletePost(post.id,post.title)"
          >
            <v-icon small>
              削除する
            </v-icon>
          </v-btn>
    </v-card-actions>
  </v-card>
  </div>
  </v-row>
  </v-container>
    </v-main>
  </v-app>
</template>

<script>
//Query
import {ALL_POSTS} from "./graphql/post-query"
//Mutation
import {CREATE_POST,UPDATE_POST,DELETE_POST} from "./graphql/post-mutation";

export default {
   name: "App",
    data: () => ({
      //本棚の中身を定義
      posts: [],
       // フォーム入力値
      post: {
        id: '',
        title: '',
        author: '',
      },
      // バリデーション
      valid: true,
      titleRules: [
        v => !!v || 'タイトルは必須項目です',
        v => (v && v.length <= 20) || 'タイトルは20文字以内で入力してください'
      ],
      authorRules: [
        v => !!v || '作者名は必須項目です',
      ],
      // ローディングの表示フラグ
      progress: false,
      // ダイアログの表示フラグ
      dialog: false,
      // 新規・更新のフラグ
      isCreate: true,
    }),
  apollo: {
    //本棚の中身
    posts: {
      //クエリを書いている部分
      query: ALL_POSTS,
        }
  },
 methods: {
    // --------------------------------
      // 新規作成
      // --------------------------------
      createPost: function () {
        if (this.$refs.form.validate()) {
          this.progress = true
          this.$apollo.mutate({
           mutation: CREATE_POST,
            variables: {
              title: this.post.title,
              author: this.post.author,
            },
          })
          .then(() => {
            //UIの更新
            this.$apollo.queries.posts.fetchMore({
              updateQuery: (previousResult, {fetchMoreResult}) => {
                // console.log(previousResult)  //変更前
                // console.log(fetchMoreResult) //変更後
                return {
                  posts: fetchMoreResult.posts
                }
              }
            })
            this.dialog = false
            this.progress = false
          }).catch((error) => {
            console.error(error)
          })
        }
      }
      ,
      // --------------------------------
      // 更新
      // --------------------------------
      updatePost: function () {
        this.progress = true
        this.$apollo.mutate({
          mutation: UPDATE_POST,
          variables: {
            id: this.post.id,
              title: this.post.title,
              author: this.post.author,
          }
        }).then(() => {
          this.$apollo.queries.posts.fetchMore({
            updateQuery: (previousResult, {fetchMoreResult}) => {
              // console.log(previousResult)  //変更前
              // console.log(fetchMoreResult) //変更後
              return {
                posts: fetchMoreResult.posts
              }
            }
          })
          this.dialog = false
          this.progress = false
        }).catch((error) => {
          console.error(error)
        })
      },
      // --------------------------------
      // 削除
      // --------------------------------
      deletePost: function (id,title) {
        console.log(id)
        console.log(title)
        if (!confirm(title + 'を削除してもよろしいですか?')) {
          return
        }
        this.progress = true
        this.$apollo.mutate({
          mutation: DELETE_POST,
          variables: {
            id: id
          }
        }).then(() => {
          this.$apollo.queries.posts.fetchMore({
            updateQuery: (previousResult, {fetchMoreResult}) => {
              // console.log(previousResult)  //変更前
              // console.log(fetchMoreResult) //変更後
              return {
               posts: fetchMoreResult.posts
              }
            }
          })
          this.progress = false
        }).catch((error) => {
          console.error(error)
        })
      },
       // --------------------------------
      // フォームのクリア
      // --------------------------------
      clear: function () {
        this.$refs.form.reset()
      },
      // --------------------------------
      // 新規追加ダイアログの表示
      // --------------------------------
      showDialogNew: function () {
        // this.clear()
        this.isCreate = true
        this.dialog = true
      },
      // --------------------------------
      // 更新ダイアログの表示
      // --------------------------------
      showDialogUpdate: function (id, title, author) {
        this.post.id = id
        this.post.title = title
        this.post.author = author
        this.isCreate = false
        this.dialog = true
      },
    }
  }
</script>

CRUD処理が出来るようになりました!!

ezgif-4-bad1856686d7.gif

Subscriptionの実装

最後にSubscriptionの実装を行います。
こちらの章を終えるとリアルタイムでの書き込みの共有が出来るようになります。
実際のアプリケーションでは通知機能を作る時などに使う部分となります。

今回もファイルを作ります。

$ touch src/graphql/post-subscription.js

Subscriptionのクエリを書きます。

post-subscription.js
import gql from 'graphql-tag'

// Subscription
export const SUBSCRIPTION_POST = gql`
subscription {
  post{
    mutation
  data{
      id
    title
    author
  }
  }
}
`

App.vueを変更します。
「apollo」のオプション内にSubscriptionの処理を追加しており、既存の投稿と同一の投稿がなかった場合、新規作成した投稿を本棚に追加しています。

App.vue

<template>
  <v-app>
    <v-main>
        <v-container>
   <!--入力フォーム-->
    <v-dialog v-model="dialog" max-width="500px">
      <v-card>
        <v-container>
          <h2 v-if="isCreate">本棚に追加する</h2>
          <h2 v-if="!isCreate">本棚を更新する</h2>
          <v-form ref="form" v-model="valid" lazy-validation>
            <!--名前-->
            <v-text-field
                v-model="post.title"
                :rules="titleRules"
                :counter="20"
                label="タイトル"
                required
            ></v-text-field>
            <v-text-field
                v-model="post.author"
                :rules="authorRules"
                :counter="20"
                label="作者"
                required
            ></v-text-field>
            <!--追加ボタン-->
            <v-btn
                v-if="isCreate"
                :disabled="!valid"
                @click="createPost"
            >
              追加
            </v-btn>
            <!--更新ボタン-->
            <v-btn
                v-if="!isCreate"
                :disabled="!valid"
                @click="updatePost"
            >
              更新
            </v-btn>
            <v-btn @click="clear">クリア</v-btn>
          </v-form>
        </v-container>
      </v-card>
    </v-dialog>


          <v-row
   style="width: 550px;"
>
    <!--ツールバー-->
    <v-toolbar  color="grey lighten-1">
      <v-toolbar-title>本棚</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn color="primary" dark class="mb-1" @click="showDialogNew">新規追加</v-btn>
    </v-toolbar>

    <!--本棚の中身-->
<div
v-for="post in posts" :key="post.id"
>
  <v-card
    class="mx-auto"
    width="550px"
    outlined
  >
    <v-list-item three-line>
      <v-list-item-content>
        <v-list-item-title class="headline mb-1">
           {{ post.title}}/{{ post.author}}
        </v-list-item-title>
        <v-list-item-subtitle>From Apollo-Server</v-list-item-subtitle>
      </v-list-item-content>
    </v-list-item>
    <!-- 編集・削除ボタン -->
    <v-card-actions>
       <v-btn
              color="success"
              small
              @click="showDialogUpdate(post.id,post.title,post.author)"
          >
            <v-icon small>
              編集する
            </v-icon>
          </v-btn>
          <v-btn
              color="error"
              small
              @click="deletePost(post.id,post.title)"
          >
            <v-icon small>
              削除する
            </v-icon>
          </v-btn>
    </v-card-actions>
  </v-card>
  </div>
  </v-row>
  </v-container>
    </v-main>
  </v-app>
</template>

<script>
//Query
import {ALL_POSTS} from "./graphql/post-query"
//Mutation
import {CREATE_POST,UPDATE_POST,DELETE_POST} from "./graphql/post-mutation";
//Subscription
import {SUBSCRIPTION_POST} from "./graphql/post-subscription";

export default {
   name: "App",
    data: () => ({
      //本棚の中身を定義
      posts: [],
       // フォーム入力値
      post: {
        id: '',
        title: '',
        author: '',
      },
      // バリデーション
      valid: true,
      titleRules: [
        v => !!v || 'タイトルは必須項目です',
        v => (v && v.length <= 20) || 'タイトルは20文字以内で入力してください'
      ],
      authorRules: [
        v => !!v || '作者名は必須項目です',
      ],
      // ローディングの表示フラグ
      progress: false,
      // ダイアログの表示フラグ
      dialog: false,
      // 新規・更新のフラグ
      isCreate: true,
    }),
  apollo: {
    //本棚の中身
    posts: {
      //クエリを書いている部分
      query: ALL_POSTS,
      //サブスクリプション
       subscribeToMore: {
        document: SUBSCRIPTION_POST,
        updateQuery:(previousResult, { subscriptionData }) =>{
            // console.log(previousResult) //前の投稿
            // console.log(subscriptionData.data) //新規作成した投稿
            // 既存の投稿と同一の投稿がなかった場合、新規作成した投稿を本棚に追加
        if (previousResult.posts.find(post => post.id === subscriptionData.data.post.data.id)) {
          return previousResult
        }else{
           return {
          posts: [
            ...previousResult.posts,
            // Add the new data
            subscriptionData.data.post.data,
          ],
        }
        }
            }
            }
        }
  },
 methods: {
    // --------------------------------
      // 新規作成
      // --------------------------------
      createPost: function () {
        if (this.$refs.form.validate()) {
          this.progress = true
          this.$apollo.mutate({
           mutation: CREATE_POST,
            variables: {
              title: this.post.title,
              author: this.post.author,
            },
          })
          .then(() => {
            //UIの更新
            this.$apollo.queries.posts.fetchMore({
              updateQuery: (previousResult, {fetchMoreResult}) => {
                // console.log(previousResult)  //変更前
                // console.log(fetchMoreResult) //変更後
                return {
                  posts: fetchMoreResult.posts
                }
              }
            })
            this.dialog = false
            this.progress = false
          }).catch((error) => {
            console.error(error)
          })
        }
      }
      ,
      // --------------------------------
      // 更新
      // --------------------------------
      updatePost: function () {
        this.progress = true
        this.$apollo.mutate({
          mutation: UPDATE_POST,
          variables: {
            id: this.post.id,
              title: this.post.title,
              author: this.post.author,
          }
        }).then(() => {
          this.$apollo.queries.posts.fetchMore({
            updateQuery: (previousResult, {fetchMoreResult}) => {
              // console.log(previousResult)  //変更前
              // console.log(fetchMoreResult) //変更後
              return {
                posts: fetchMoreResult.posts
              }
            }
          })
          this.dialog = false
          this.progress = false
        }).catch((error) => {
          console.error(error)
        })
      },
      // --------------------------------
      // 削除
      // --------------------------------
      deletePost: function (id,title) {
        console.log(id)
        console.log(title)
        if (!confirm(title + 'を削除してもよろしいですか?')) {
          return
        }
        this.progress = true
        this.$apollo.mutate({
          mutation: DELETE_POST,
          variables: {
            id: id
          }
        }).then(() => {
          this.$apollo.queries.posts.fetchMore({
            updateQuery: (previousResult, {fetchMoreResult}) => {
              // console.log(previousResult)  //変更前
              // console.log(fetchMoreResult) //変更後
              return {
               posts: fetchMoreResult.posts
              }
            }
          })
          this.progress = false
        }).catch((error) => {
          console.error(error)
        })
      },
       // --------------------------------
      // フォームのクリア
      // --------------------------------
      clear: function () {
        this.$refs.form.reset()
      },
      // --------------------------------
      // 新規追加ダイアログの表示
      // --------------------------------
      showDialogNew: function () {
        // this.clear()
        this.isCreate = true
        this.dialog = true
      },
      // --------------------------------
      // 更新ダイアログの表示
      // --------------------------------
      showDialogUpdate: function (id, title, author) {
        this.post.id = id
        this.post.title = title
        this.post.author = author
        this.isCreate = false
        this.dialog = true
      },
    }
  }
</script>


確認してみましょう。ブラウザのウィンドウを2つ用意します。

subscriptionVue.gif

こちらも上手くいきました!!!

おわりに

以上、今回はGraphQLをVueで扱ってみました。
基本的なCRUD機能は抑えたので、今回の内容を元に発展的なアプリケーションを作ることが可能かと思います。

それでは、また😊

12
8
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
12
8