新卒エンジニアがVue+GraphQL+ServerlessでTodoアプリ作ってみた

# はじめに
## この記事について
1. 新卒エンジニアが全く触ったことのなかった技術でTodoアプリを作ってみた話
1. ややチュートリアル形式
1. cliなどを使って楽に構築
1. 各技術についての説明は少なめ
1. vueやGraphQLの雰囲気を掴みたい方向け？
1. 間違いの指摘やより効率的な書き方、今後の学習法などのアドバイスは歓迎

# Todoアプリ概要
## 要件
- ユーザーの認証が出来ること
- TodoのCRUD操作が出来ること

## 技術
- Vue.js
- graphcool(serverless GraphQL)
- Apollo（クライアント側でGraphQLを扱いやすくするためのもの)

## 開発環境
- OS: Ubuntu 16.04.3 LTS
- node: v8.1.4
- npm: 5.5.1
- docker: 17.0.5.0-ce

# Todoアプリ作成
完成品は[こちら](https://github.com/SakuraCase/practice-todo)になります。

## graphcoolの用意
まずはGraphQLの環境から用意していきます。

```
mkdir todo
cd todo
npm install -g graphcool
graphcool init graphcool
```

これで`graphcool`という名前のディレクトリが作成されて、準備完了です。簡単！

## vueの用意
次にvueの環境を用意していきます。
後々使うものも入れてしまいます。

```
npm install -g vue-cli

# 色々質問されますが今回私は基本的にそのままで、テスト関係だけnoにしました
vue init webpack vue
cd vue
npm install
npm install --save vue-apollo apollo-client-preset apollo-link-context graphql graphql-tag

```

これで`vue`という名前のディレクトリが作成されて、準備完了です。簡単！！

さて、それではまずDB側から作っていきたいと思います。


# graphcool側の実装
## スキーマ作成
今回のTodoアプリではユーザーの認証が出来ることが要件に含まれています。
graphcoolでは認証を行うためのテンプレートが用意されているため、
簡単に認証機能を実装することが出来ます。

```
cd graphcool
graphcool add-template graphcooltemplates/auth/email-password
```

これで認証に必要なものが追加されました。すごい！！！
（ちなみに、graphcoolのサイトのチュートリアルではfacebookを利用したログイン方法も載っていました）

ただ、追加してすぐは機能を利用することが出来ません。
利用するためには`graphcool.yml`と`types.graphql`のコメントアウトを外す必要があります。
コメントアウトを外すついでにTodoアプリのテーブル設計も行います。

```yaml:graphcool/graphcool.yml
types: ./types.graphql

functions:
  signup:
    type: resolver
    schema: src/email-password/signup.graphql
    handler:
      code: src/email-password/signup.ts

  authenticate:
    type: resolver
    schema: src/email-password/authenticate.graphql
    handler:
      code: src/email-password/authenticate.ts

  loggedInUser:
    type: resolver
    schema: src/email-password/loggedInUser.graphql
    handler:
      code: src/email-password/loggedInUser.ts

permissions:
  - operation: "*"
```

graphcool.ymlについてはコメントアウトを外すだけです。
これで認証の機能が使えるようになります。

```graphcool/types.graphql
type User @model {
  id: ID! @isUnique
  createdAt: DateTime!
  updatedAt: DateTime!

  email: String! @isUnique
  password: String!
  todos: [Todo!]! @relation(name: "UserTodos")
}


type Todo @model {
  id: ID! @isUnique
  createdAt: DateTime!
  updatedAt: DateTime!

  title: String!
  done: Boolean!
  author: User! @relation(name: "UserTodos")
}
```

`types.graphql`でスキーマ定義を行います。
Userは`システム項目`と`email`,`password`,`todos`を持ちます。
Todoは同じ`システム項目`と`title`,`done`,`author`を持ちます。
titleはtodoの名前、doneは完了したtodoか、authorは誰のTodoか。
基本的なTodoアプリのテーブル設計と同じかと思います。
ちなみに`!`は、必須項目(not null)であることを意味しています。

## DB作成
さて、スキーマ定義が終わったのでDBを立てていきます。
今回はDocker上に作成します。
私はDockerにあまり慣れていませんが、問題なく利用できました。
困った時の呪文としては「データボリュームの全削除」です。

```
graphcool local up

# Please choose the cluster you want to deploy toと
# 聞かれるので、一番下のlocalを選択。その後はそのままでok
graphcool deploy
```

成功したら色々表示されると思います。
そして下の方に`Simple API:`というものが書かれていると思うので、そのurlを控えておきます。
vueから接続するときに必要になります。
控え忘れてしまった場合は`graphcool info`などで確認できます。

さあこれでGraphQLが利用できます！やったね！

## ユーザー作成
Todoアプリ内でユーザー登録は今回作成していません。
なので、別の場所からユーザーを登録します。次のコマンドを打ちましょう！

```
graphcool playground
```

すると以下のような画面が開かれます。
ここから直接GraphQLを利用出来ます。
(ちなみに、右上の歯車マークから**VIM MODE**も選択できます。)

![graphcool1.png](https://qiita-image-store.s3.amazonaws.com/0/31732/170c77c4-0fb8-3f6a-4c3f-a520ceeb1c8d.png)

ではユーザーを作成してみたいと思います。
今回認証機能を追加しているので、`sigupUser`というmutationsを利用します。
mutationsとはquery以外（更新削除作成）の操作のことです（多分）
右側に表示されているSCHEMAのMUTATIONSの欄を見ていただけると雰囲気がなんとなくわかると思います。

以下のように書いて、真ん中の実行ボタンを押します。

```
mutation {
  signupUser(email:"test@mail.com" password:"test")
  {
    id
    token
  }
}
```

![graphcool2.png](https://qiita-image-store.s3.amazonaws.com/0/31732/31e584e5-3997-291c-c001-9acd268b2008.png)

`signupUser()`が利用するAPIの名前で`()`内が引数です。
`{}`内は結果を返してくれます。
今回の場合はユーザーを作成した後`id`と`token`を返しています。
これでユーザーが作成されました。

さて、次に作成したばかりのユーザー情報を見てみます。
idをコピーして、以下のqueryを書きます。

```
query {
  User(id:"cjatrk2ti02eb0189w0zgclot")
  {
    id
    createdAt
    updatedAt
    email
    password
    todos{
      id
      title
    }
  }
}
```

![graphcool3.png](https://qiita-image-store.s3.amazonaws.com/0/31732/7b9f6303-4cdd-d2f3-b7f6-3200dba61b2a.png)


ちゃんと作成出来ていました！やったね！


# Vue側の実装
さて、いよいよVue側の実装になります。
Vueですが、業務でAngularに触れたことがあって「雰囲気似ている…？」という理由からチュートリアルをあまり読まずに使ったところ結構はまりました。
よろしくない実装もあると思いますのであしからず。

## ログイン機能
まずはログインです。
変更ファイルは`vue/src/main.js`、`vue/App.vue`です。
追加ファイルは`vue/src/component`配下の`Login.js`と`Logout.js`と、
`vue/src/constants`配下に`graphql.js`と`settings.js`です。

まず`App.vue`を見ます。
こちらがいわゆるindexページです。
styleはそのまま利用しているのでここでは省いています。

```html:vue/src/App.vue
<template>
  <div id="app">
    <logout v-if="isLoggedIn"></logout>
    <login v-else></login>

    <router-view/>
  </div>
</template>

<script>
  import Logout from './components/Logout'
  import Login from './components/Login'

  export default {
    name: 'app',
    components: {
      Logout,
      Login
    },
    computed: {
      isLoggedIn () {
        return this.$root.$data.token
      }
    }
  }
</script>
```

ログインとログアウトのコンポーネントを読み込んで表示しているだけです。
`isLoggedIn()`でtokenが保存されているかによって表示を切り替えています。


```html:vue/src/components/Logout.vue
<template>
  <button @click="logout()">Logout</button>
</template>

<script>
  import {AUTH_TOKEN} from '../constants/settings'

  export default {
    name: 'Logout',
    methods: {
      logout () {
        localStorage.removeItem(AUTH_TOKEN)
        this.$root.$data.token = localStorage.getItem(AUTH_TOKEN)
        this.$router.push({path: '/'})
      }
    }
  }
</script>
```

次に`Logout.vue`ですが、これもシンプルで、
ログアウトボタンが1個あって、押されたらlocalstorageに保存しているtoken情報を削除してトップへ移動させているだけです。

```html:vue/src/components/Login.vue
<template>
  <div>
    <input v-model="email" type="text" placeholder="Email">
    <input v-model="password" type="password" placeholder="Password">
    <button @click="confirm()">Login</button>
  </div>
</template>

<script>
  import {AUTH_TOKEN} from '../constants/settings'
  import {LOGIN_MUTATION} from '../constants/graphql'

  export default {
    name: 'Login',
    data () {
      return {
        email: '',
        password: ''
      }
    },
    methods: {
      confirm () {
        const {email, password} = this.$data

        this.$apollo.mutate({
          mutation: LOGIN_MUTATION,
          variables: {
            email,
            password
          }
        }).then((result) => {
          const token = result.data.authenticateUser.token
          localStorage.setItem(AUTH_TOKEN, token)
          this.$root.$data.token = localStorage.getItem(AUTH_TOKEN)

          this.$router.push({path: '/todos'})
        }).catch((error) => {
          alert(error)
        })
      }
    }
  }
</script>
```
ログインです。
templateとしてはemailとpasswordを入力してもらうフォームと送信ボタンです。
フォームで入力された値は`data()`の`email`と`password`にバインドされます。

`methods`としては、送信ボタンが押された時にユーザーを認証する処理の`confirm()`があります。
`this.$apollo.mutate`がGraphQLのmutationを呼び出しています。
mutationの実態としては`vue/src/constants/graphql.js`に定義されている以下です。

```vue/src/constants/graphql.js
export const LOGIN_MUTATION = gql`
  mutation LoginMutation($email: String!, $password: String!) {
    authenticateUser(email: $email, password: $password) {
      id
      token
    }
  }
`
```

これで認証が上手く行ったら`id`と`token`が返ってくるので、`token`をlocalstorageに保存しています。
そしてその後にtodoのページに飛ぶ形になります。


さて、ここで保存したtokenですが利用している場所はどこかというと`vue/src/main.js`になります。

```vue/src/main.js
import {AUTH_TOKEN, DB_URL} from './constants/settings'
import {ApolloClient} from 'apollo-client'
import {HttpLink} from 'apollo-link-http'
import {setContext} from 'apollo-link-context'
import {InMemoryCache} from 'apollo-cache-inmemory'
import Vue from 'vue'
import App from './App'
import router from './router'
import VueApollo from 'vue-apollo'

Vue.config.productionTip = false
Vue.use(VueApollo)
let token

const authLink = setContext((_, { headers }) => {
  token = localStorage.getItem(AUTH_TOKEN)
  return {
    headers: {
      authorization: token ? `Bearer ${token}` : null
    }
  }
})

const httpLink = new HttpLink({
  uri: DB_URL
})

const apolloClient = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
})

const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
  defaultOptions: {
    $loadingKey: 'loading'
  }
})

/* eslint-disable no-new */
new Vue({
  el: '#app',
  apolloProvider,
  router,
  data: {
    token
  },
  template: '<App/>',
  components: { App }
})
```

`authLink`で、リクエストヘッダーにtokenを追加しています。
また、`httpLink`で前に自分で立てたDBのSimple APIのurlが指定されています。
この2つを`ApolloClient`の`link`として渡しています。

さて、これで以下のようなログインログアウトが出来るようになりました。

ログイン前：
![Screenshot-2017-12-5 vue(1).png](https://qiita-image-store.s3.amazonaws.com/0/31732/b32ebf36-9262-a108-4d09-5bffe3b3a1ef.png)

ログイン後：
![Screenshot-2017-12-5 vue(2).png](https://qiita-image-store.s3.amazonaws.com/0/31732/8eb41b48-52bd-913e-3ece-8859b8081157.png)


デザインのデの字もなく寂しいですね。残念です。

## Todo機能
ようやく本題のTodo機能です。

todoは
`vue/src/components`配下の`Todos.vue`, `Todo.vue`, `CreateTodo.vue`になります。
基本的に`Todos.vue`でデータの取得を行い、その子コンポーネントである`Todo.vue`と`CreateTodo.vue`でDBの更新を行っています。

まず子コンポーネントの`Todo.vue`から見ていきます。

```html:vue/src/components/Todo.vue
<template>
  <div>
    <input type="checkbox" @click="done" :checked="todo.done">
    <input v-model="title">
    <button @click="edit">編集</button>
    <button @click="del">削除</button>
  </div>
</template>

<script>
  import {UPDATE_TODO, DELETE_TODO} from '../constants/graphql'

  export default {
    name: 'Todo',
    props: ['todo'],
    data () {
      return {
        title: this.todo.title
      }
    },
    methods: {
      done () {
        this.$apollo.mutate({
          mutation: UPDATE_TODO,
          variables: {
            id: this.todo.id,
            done: !this.todo.done
          }
        })
      },
      edit () {
        this.$apollo.mutate({
          mutation: UPDATE_TODO,
          variables: {
            id: this.todo.id,
            title: this.title
          }
        })
      },
      del () {
        this.$apollo.mutate({
          mutation: DELETE_TODO,
          variables: {
            id: this.todo.id
          }
        }).then(() => {
          this.$emit('refresh')
        })
      }
    }
  }
</script>
```

やや長いですが、中身は非常にシンプルです。
親側からは1行のTodoデータが渡されます。
`template`ではそのデータを表示、加工用ボタンを設置しています。
`methods`の`done`,`edit`,`del`はDBを更新してします。
`done`なら完了にして（未完了にして）更新、`edit`ならtitleの変更です。
それとこれは親側の話になりますが`del`の`this.$emimt('refresh')`が起こるとデータの再取得を行います。
本来は再取得ではなくリストを変更したほうがいいのでしょうが、実装の仕方がいまいちわからず、この形に落ち着きました。
ちなみに、`done`と`edit`の場合だとイベント起こさなくても勝手に変わってくれます。
どうしてそういう動きをするのかについてはわかりませんでした。
どなたか教えてください！！


さて、次に親コンポーネントの`Todos.vue`です。

```html
<template>
  <div>
    <h4 v-if="loading">Loading Todos...</h4>
    <div v-else>
      <h3>タスク追加</h3>
      <create-todo @refresh="resetStore"></create-todo>

      <h3>未完了タスク</h3>
      <todo v-for="(todo, i) in tasks" @refresh="resetStore" :key="todo.id" :todo="todo"></todo>

      <h3>完了タスク</h3>
      <todo v-for="(todo, i) in done" @refresh="resetStore" :key="todo.id" :todo="todo"></todo>
    </div>
  </div>
</template>

<script>
  import {USER_TODOS, LOGGED_IN_USER} from '../constants/graphql'
  import Todo from './Todo'
  import CreateTodo from './CreateTodo'

  export default {
    name: 'Todos',
    data () {
      return {
        todos: [],
        user: '',
        loading: 0
      }
    },
    components: {
      Todo,
      CreateTodo
    },
    computed: {
      done () {
        return this.todos.filter(todo => todo.done === true)
      },
      tasks () {
        return this.todos.filter(todo => todo.done === false)
      }
    },
    apollo: {
      user: {
        query: LOGGED_IN_USER,
        update (data) {
          return data.loggedInUser.id
        }
      },
      todos: {
        query: USER_TODOS,
        variables () {
          return {
            id: this.user
          }
        },
        update (data) {
          return data.User.todos
        }
      }
    },
    methods: {
      resetStore () {
        this.$apollo.provider.defaultClient.resetStore()
      }
    }
  }
</script>
```

`template`では新規登録欄の表示とtodo一覧の表示を行っています。
`computed`で完了タスクと未完了タスクを分けて、分けられたものをそれぞれ表示しています。

そして`apollo`でGraphQLのqueryを実行しています。
`user`はヘッダーに詰められたtokenを元に、現在のユーザーを返すqueryになっています。
`vue/src/main.js`で詰めたtokenがここで利用されています。
（ちなみに、graphcoolのplaygroundでこのquery(LoggedInUser)を利用するときは、左下の`HTTP HEADERS(0)`からautorizationとBearer tokenを渡してあげる必要があります）

`todos`では、現在のユーザーのtodo一覧を取得します。
variablesをリアクティブにしてuserが変更されるたびに実行しています。
このようにしているのは、userを取得した後todosを取得する、という方法がわからなかったためです。
実はこの`todos`実行されると「userがないんだけど！」って1回怒られます。


さあ、ここまでくれば、あとは同じ要領で`vue/src/components/CreateTodo.vue`を実装して、todosを`router`に追加して完成です。
以下のようなTodoアプリが完成しました！やったー！

![Screenshot-2017-12-5 vue(3).png](https://qiita-image-store.s3.amazonaws.com/0/31732/30902713-102e-f24b-f1c8-69b72d5d3e90.png)

style指定してないからやっぱり寂しい。けど[完成](https://github.com/SakuraCase/practice-todo)です！！！


# おわりに
ここまでお付き合いありがとうございます。
何かしらお役に立てば嬉しいです。

作ってみた所感としては、
apolloの情報が上手く見つけられず少し辛かったこと、
vueで配列操作が思い通りに出来ず少し辛かったこと、
そしてそもそもweb開発の知識が足りてなくてtokenの使い方とか全然わからなかったことなどなどあるのですが、
総括して、触ったことない技術に触れられて楽しかった！といった具合です。

まあ新卒なのでほとんどが触ったことない技術なのですが！
記事にするかはわかりませんが、次は今回作ってみたこれをAWSに乗っけてみたりもしようかなって思っています。

ではでは。

## 参考
[GraphQL入門 - 使いたくなるGraphQL](https://qiita.com/bananaumai/items/3eb77a67102f53e8a1ad)
[Vue + Apollo Tutorial - Introduction (情報がやや古いので注意)](https://www.howtographql.com/vue-apollo/0-introduction/)
[Vue](https://jp.vuejs.org/v2/guide/)
[Graphcool Docs](https://www.graph.cool/docs/)
[vue-apollo](https://github.com/Akryum/vue-apollo)