はじめに
この記事について
- 新卒エンジニアが全く触ったことのなかった技術でTodoアプリを作ってみた話
- ややチュートリアル形式
- cliなどを使って楽に構築
- 各技術についての説明は少なめ
- vueやGraphQLの雰囲気を掴みたい方向け?
- 間違いの指摘やより効率的な書き方、今後の学習法などのアドバイスは歓迎
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アプリ作成
完成品はこちらになります。
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アプリのテーブル設計も行います。
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についてはコメントアウトを外すだけです。
これで認証の機能が使えるようになります。
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も選択できます。)
ではユーザーを作成してみたいと思います。
今回認証機能を追加しているので、sigupUser
というmutationsを利用します。
mutationsとはquery以外(更新削除作成)の操作のことです(多分)
右側に表示されているSCHEMAのMUTATIONSの欄を見ていただけると雰囲気がなんとなくわかると思います。
以下のように書いて、真ん中の実行ボタンを押します。
mutation {
signupUser(email:"test@mail.com" password:"test")
{
id
token
}
}
signupUser()
が利用するAPIの名前で()
内が引数です。
{}
内は結果を返してくれます。
今回の場合はユーザーを作成した後id
とtoken
を返しています。
これでユーザーが作成されました。
さて、次に作成したばかりのユーザー情報を見てみます。
idをコピーして、以下のqueryを書きます。
query {
User(id:"cjatrk2ti02eb0189w0zgclot")
{
id
createdAt
updatedAt
email
password
todos{
id
title
}
}
}
ちゃんと作成出来ていました!やったね!
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はそのまま利用しているのでここでは省いています。
<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が保存されているかによって表示を切り替えています。
<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情報を削除してトップへ移動させているだけです。
<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
に定義されている以下です。
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
になります。
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
として渡しています。
さて、これで以下のようなログインログアウトが出来るようになりました。
デザインのデの字もなく寂しいですね。残念です。
Todo機能
ようやく本題のTodo機能です。
todoは
vue/src/components
配下のTodos.vue
, Todo.vue
, CreateTodo.vue
になります。
基本的にTodos.vue
でデータの取得を行い、その子コンポーネントであるTodo.vue
とCreateTodo.vue
でDBの更新を行っています。
まず子コンポーネントの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
です。
<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アプリが完成しました!やったー!
style指定してないからやっぱり寂しい。けど完成です!!!
おわりに
ここまでお付き合いありがとうございます。
何かしらお役に立てば嬉しいです。
作ってみた所感としては、
apolloの情報が上手く見つけられず少し辛かったこと、
vueで配列操作が思い通りに出来ず少し辛かったこと、
そしてそもそもweb開発の知識が足りてなくてtokenの使い方とか全然わからなかったことなどなどあるのですが、
総括して、触ったことない技術に触れられて楽しかった!といった具合です。
まあ新卒なのでほとんどが触ったことない技術なのですが!
記事にするかはわかりませんが、次は今回作ってみたこれをAWSに乗っけてみたりもしようかなって思っています。
ではでは。
参考
GraphQL入門 - 使いたくなるGraphQL
Vue + Apollo Tutorial - Introduction (情報がやや古いので注意)
Vue
Graphcool Docs
vue-apollo