[vue3/typeScript ]Vue Apollo で GraphQLを試したサンプル解説

# この記事の概要

Vue3でTypeScriptを使いGraphQLを試してみた

- Vue3対応
- Vue Apollo v3（vue3に正式対応しているのはv4から 開発中）
- GraphQLを実行するまでのサンプル解説

# GraphQLについて

https://graphql.org/

> APIのためのクエリー言語であり、既存のデータを使ってクエリーを実行するためのランタイム。

クライアントからサーバーのデータを取得する場合REST APIが主な手段だった
REST APIとの比較については以下記事をお勧めする

https://hasura.io/learn/ja/graphql/intro-graphql/graphql-vs-rest/

# サンプル

https://github.com/shiho-hoshino/graphql-vue3-sample

# 目次

1. [GraphCMS](https://hygraph.com/)で簡易サーバー作成
1. Vue3にVue Apolloの環境を作成
1. VueからGraphQL を実行（GET）
  ※ POST、PUT、DELETEの サンプルはGitへ

# 【1】GraphCMSで簡易サーバー作成

[GraphCMS](https://hygraph.com/)はGraphQLでレスポンスを返してくれるCMS  
これを使用してDBサーバを作成する

1. アカウントとプロジェクトの作成
1. Modelの作成
1. Fieldの作成
1. データの追加
1. Tokenの作成と権限の追加
1. Content API URLの取得

### [1-1] アカウントとプロジェクトの作成

- Create a new project 設定
  - Template: Blank
  - Project name: test-project
  - Project description: なし
  - Select region: Japan
  - Plan: Free forever
  - Members: -
    - 自分以外にメンバーを追加したい場合のみ設定

### [1-2] Modelの作成

- Schema > MODELS > Add
- Create Model 設定
  - Display name: Task
  - API ID: Task
  - Plural API ID: Tasks
  - Discription: なし

### [1-3] Fieldの作成

- FieldのTypeを選択する
  - MODELS > task > Add Fields > [Type選択]
- Create Field 設定1
  - Type: Single line text
  - Display name: title
  - API ID title
- Create Field 設定2
  - Type: Multi line text
  - Display name: discription
  - API ID discription
- Create Field 設定3
  - Type: Boolean
  - Display name: task_status
  - API ID task_status

![スクリーンショット 2022-08-27 17.00.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/f0694df6-601d-7e93-a750-26355fbec88c.png)

 
### [1-4] データの追加

- Content > DEFAULT VIEWS > [Model Name(Workflow)] > Create Entry

![スクリーンショット 2022-08-27 17.02.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/5e9ff92e-c4d7-0215-da3b-7c68bc29a9a8.png)


### [1-5] Tokenの作成と権限の追加

Project Settings > API Access　ページでTokenを作成
Project Settings > API Access > Permanent Auth Tokens > Create token

![graphcms_token_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/8c4c65ec-0fcb-c153-68d1-bca606ec7bd1.png)

作成したTokenに権限を設定する
![graphcms_token_permission_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/1e28e8c5-27b4-f49b-3be3-b55325bf44f3.png)
権限を与えるModelを作成したTaskにして追加
![graphcms_token_permission_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/c59b8eed-fcb8-e283-94ec-505b425e573a.png)
![graphcms_token_permission_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/5654d216-28cf-a72e-dee7-ca669c1a795b.png)

### [1-6] Content API URLの取得

- Content API URL
  - Project Settings > API Access > Endpoints > Content API
  ![graphcms_api_url.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/ee9e3640-36ec-ae15-0cf0-abb929e6e373.png)


# 【2】Vue3 プロジェクトの作成と設定

https://v3.ja.vuejs.org/guide/typescript-support.html#%E9%96%8B%E7%99%BA%E3%83%84%E3%83%BC%E3%83%AB

1. Vue3プロジェクトの作成
1. vue-routerの設定
1. vue-apolloの設定
1. eslintの設定


### [2-1] Vue3プロジェクトの作成

```
# yarn create vite <プロジェクト名> --template vue-ts    
yarn create vite vite_ts --template vue-ts 

cd <プロジェクト名>
yarn
yarn dev
```

作業しやすいようにポートとエイリアスを設定しておく

vite.config.ts
```ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
  server: {
    port: 8888,
  },
  resolve: {
    alias: {
      "~/": `${__dirname}/src/`,
    },
  },
});

```

tsconfig.jsonにもpathの設定を追加
```json
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM"],
    "skipLibCheck": true,
    "baseUrl": "./src",
    "paths": {
      "~/*": [
        "./*"
      ]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
```

### [2-2] vue-routerの設定

```node
yarn add vue-router@4 
```

以下のファイルを作成

src/App.vue（router-view部分がpageごとに切り替わる）
```
<template>
  <router-view />
</template>
<style>
table{
  width: 100%;
  border-spacing: 0;
}

table th{
  border-bottom: solid 2px #ccc;
  padding: 10px 0;
}

table td{
  border-bottom: solid 1px #ddd;
  text-align: center;
  padding: 5px 0;
}

input[type="text"] {
  width: 100%;
}

textarea {
  width: 100%;
}

button {
    display: inline-block;
    width: 100%;
    max-width: 100px;
    margin: auto;
    padding: 4px 8px;
    border: none;
    border-radius: 4px;
    background-color: #269dff;
    color: #fff;
    font-size: 12px;
    font-weight: bold;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    cursor: pointer;
    border: 2px solid transparent;
}
button:hover {
  opacity: 0.5;
}
button.small {
  padding: 2px 4px;
  font-size: 10px;
}
button:disabled {
  background-color: #bababa;
  cursor: default;
  opacity: 1;
}
button.outlined:not(:disabled) {
  background-color: #fff;
  border: 1px solid #fb4444;
  color: #fb4444;
}
button.delete:not(:disabled) {
  background-color: #fb4444;
}
button.complete:not(:disabled) {
  background-color: #00b209;
}
</style>
```

src/router.ts（[2-3] vue-apolloの設定時に main.tsで読み込む）
```
import { createRouter, createWebHistory } from "vue-router";
import Home from "./pages/index.vue";
import getPage from "./pages/get.vue";

const routes = [
  { path: "/", name: "home", component: Home },
  { path: "/get", name: "get", component: getPage },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;
```

### [2-3] vue-apolloの設定

- 追加 plugin
  - "@vue/apollo-option": "^4.0.0-alpha.20"
  - "vue-apollo": "^3.1.0"
  - "graphql-tag": "^2.12.6"
  - "vue-cli-plugin-apollo": "^0.22.2"

```
yarn add -D graphql-tag vue-cli-plugin-apollo vue-apollo @vue/apollo-option
```

.env.localに「TokensとContent API URLの取得」で控えた情報を設定
```
VITE_GRAPHQL_AUTH_TOKEN=[あなたのTOKEN]
VITE_GRAPHQL_HTTP=[あなたのAPI ENDPOINT]
```

- main.ts
  - vue-apolloの設定コードを追加
  - vue-routerを読み込み

```ts
import "./style.css";
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client/core";
import { createApolloProvider } from "@vue/apollo-option";
import { createApp } from "vue";

import router from "./router";
import App from "./App.vue";

const AUTH_TOKEN = import.meta.env.VITE_GRAPHQL_AUTH_TOKEN || "";
const httpEndpoint =
  import.meta.env.VITE_GRAPHQL_HTTP || "http://localhost:4000/graphql";
const httpLink = new HttpLink({
  uri: httpEndpoint,
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${AUTH_TOKEN}`,
  },
});

/**
 * Create the Apollo client
 */
const cache = new InMemoryCache();
const apolloClient = new ApolloClient({
  cache,
  link: httpLink,
});

/**
 * Create the Apollo provider
 */
const apolloProvider = createApolloProvider({
  defaultClient: apolloClient,
});

/**
 * Add the provider to your app
 */
const app = createApp(App);
app.use(router);
app.use(apolloProvider);

app.mount("#app");
```

### [2-3] eslintの設定

- 追加 plugin
  - "eslint": "^8.21.0"
  - "eslint-plugin-vue": "^9.3.0"
  - "@vue/eslint-config-typescript": "^11.0.0"
  - "@typescript-eslint/eslint-plugin": "^5.32.0"
  - "@typescript-eslint/parser": "^5.32.0"

```
yarn add -D eslint eslint-plugin-vue @vue/eslint-config-typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin
```

eslintの設定ファイル(.eslintrc.yml)を追加
```
touch .eslintrc.yml
```

.eslintrc.yml
```yml
env:
  browser: true
  es2021: true
extends:
  - 'plugin:vue/vue3-recommended'
  - 'eslint:recommended'
  - '@vue/typescript/recommended'
parser: 'vue-eslint-parser'
parserOptions:
  parser: '@typescript-eslint/parser'
  ecmaVersion: 12
plugins:
  - vue
  - '@typescript-eslint'
```

#### Vscodeの拡張機能でVeturを利用してる場合

vue3から利用できる`<script setup lang="ts">`にVeturは未対応（2022.08.07)
以下手順で拡張機能を変更する

- Veturを利用するワークスペースでだけ無効化
  - Disable(Workspace)
  ![スクリーンショット 2022-08-07 16.41.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/fec1b2c9-2135-55cc-2792-8941fcb9c0db.png)
- `Vue Language Features (Volar)`を追加
    ![スクリーンショット 2022-08-07 16.41.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/b46496f0-d35c-0045-a223-e66426da04be.png)

# 【3】VueからGraphQLを実行（GET）

設定が終わったので実際に動かしてみる

1. GraphQL文を書く（GET ワークフローの全ての情報を取得)
1. Vue Apolloで実行する

### [3-1] GraphQL文を書く（GET)

src/constants/task.ts
```ts
export const ALL_WORKFLOWS = gql`
query allWorkflows {
    workflows(orderBy: id_ASC) {
      id
      title
      description
      createdAt
    }
  }
`;
```

titleとdescriptionは自分で設定した項目
idとcreatedAtはpost時に自動で追加される項目

`query allWorkflows`
「allWorkflows」は好きな名称でよい
このQL文はワークフローの全ての情報を取得するためのものなので「allWorkflows」とした

`workflows(orderBy: id_ASC) {`
「workflows」はModel名の複数形にすることで「複数データの取得」となる
「orderBy: 項目名_昇順・降順」でソート順を決められる
「id_ASC」はidの昇順、「createdAt_DESC」なら作成した日時順で降順となる

### [3-2] Vue Apolloで実行する

apolloの設定はすでにしてあるので
Vueでアポロを使って先ほど作成したQL文章を渡して実行する

src/pages/get.vue
```vue
<template>
  <div>
    <table>
      <tr>
        <th width="200">
          ID
        </th>
        <th width="200">
          TITLE
        </th>
        <th width="300">
          DESCRIPTION
        </th>
        <th width="100">
          STATUS
        </th>
      </tr>
      <tr
        v-for="task in tasks"
        :key="task.id"
      >
        <td>{{ task.id }}</td>
        <td>{{ task.title }}</td>
        <td>{{ task.description }}</td>
        <td>{{ task.task_status }}</td>
      </tr>
    </table>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

import {
  ALL_TASKS,
} from '~/constants/task';

interface task {
  id: string,
  title: string
  description: string
  task_status: boolean
}

interface Data {
  tasks: task[],
}

export default defineComponent({
  name: 'TodoList',
  data(): Data {
    return {
      tasks: [],
    };
  },
  apollo: {
    tasks: ALL_TASKS,
  },
});
</script>
```

これを実行する
```
yarn dev
```

以下のように表示される。
データの取得が行えた。

![スクリーンショット 2022-08-27 15.56.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/159cbcd9-6204-315e-8abd-34a235d3c4e8.png)


以上