LoginSignup
48
33

More than 3 years have passed since last update.

人をダメにするHasura GraphQL EngineとVue.jsを使って爆速15分でTodoアプリを作る

Last updated at Posted at 2019-12-08

(こんなに統一感のないバラエティに富んだアドベントカレンダーがあっていいのか・・・)

Hasuraとは??

Hasuraとは、Postgresと連携したGraphQLベースのAPIサーバーです。
ドキュメントのQuickStartに従うだけでPostgresサーバーも含めて2秒で構築できます。
ARMテンプレートも完備されており、クリックだけでAzureにデプロイできたりするので、ITエンジニアをダメにするビーンバッグチェア的な力を秘めています。

今回はAPIをHasura、画面をVueで超簡単なTodoアプリを15分で構築します。
API側はHasuraが全てよしなにやってくれるので、一行もコードを書きません。

万が一のために雑に作っちゃったコードを置いてあるので、万が一シチュエーションの場合は以下からご覧ください。
https://github.com/yoshi-sato/hasura_todo_adventcalendar2019

なぜ15分??

あなたは都内海側某所に居を構え、最近パートナーとの2人暮らしを始めたカナリイケてるITエンジニアです。
あなたとあなたのパートナーは休日の夕方に、南船橋にあるIKEAに家具日用品お菓子その他諸々を買いに行く約束をしていました。
IKEAで買うものについて話し合っていたあなたは、パートナーの口から次々と出てくる欲しいものを書き留める必要があることに気がつきました。

「HasuraとVueで買い物リストを作るしかない・・・」

しかし約束の時間まではわずか15分。
あなたは一度取り交わした約束は守る紳士なので、今更買い物リストを作り終えるまで待ってくれなどとはいえません。
果たして15分でTodoアプリを完成させ、パートナーの信用を失わず無事にIKEAに繰り出すことができるのでしょうか。

APIサーバーとDBサーバーを建てる

あなたは急いでMacBookを開き、HasuraをDocker環境で動かす準備を始めます。
https://docs.hasura.io/1.0/graphql/manual/getting-started/docker-simple.html#step-1-get-the-docker-compose-file

mkdir todo_hasura
cd todo_hasura
wget https://raw.githubusercontent.com/hasura/graphql-engine/master/install-manifests/docker-compose/docker-compose.yaml
docker-compose up -d
docker-compose ps

QuickStartに書いてある手順を丸まま自分のターミナルにコピペし、全部よしなにやってくれるdocker-composeファイルでHasuraとPostgresサーバーを起動します。
ここで目ざといあなたはgrahql-engineがポート8080番で動作していることに注目します。

            Name                          Command              State           Ports
---------------------------------------------------------------------------------------------
todo_hasura_graphql-engine   graphql-engine serve            Up      0.0.0.0:8080->8080/tcp
todo_hasura_postgres         docker-entrypoint.sh postgres   Up      5432/tcp

そしておもむろにWebブラウザを開いてlocalhost:8080にアクセスし、なんか動いていることを確認し満足感を覚えました。
同時に、エンドポイントに目がないあなたはここで表示されたエンドポイントをメモします。

image.png

あなたは開いた画面のDATAタブを押すと、画面左にAdd Tableというボタンが現れることに気がつきました。
ここに必要事項を入力してSaveすると、Postgresサーバーにテーブルが作成されるのです。
あなたは今回はオートインクリメント付きIntegerタイプのidカラムと、Textタイプのtitleカラムを作成しました。
Primary keyはidカラムです。
レコードの追加や削除もこの画面で行えます。

image.png

画面の雛形を作る

Hasuraはどうやら動いているようですが、画面がなければ買い物リストを見るためにいちいちGraphQLクエリを手動で打ち込まなければいけません。
勘の良いあなたにとって、いくら広いとは言え休日の混雑したIKEA店内で、そんなことをする余裕などないことは火を見るよりも明らかでした。
それに、一般人の前でMacBookを開きかちゃかちゃとGraphQLクエリを打ち込んでいてはIKEAの設備を操って人々を傷つけようとしている悪のハッカーだと思われかねません。

あなたはvue-cliの力を借りて雛形を作成し、GraphQL APIのクライアントライブラリApolloと、その他必要なライブラリを次々とyarn addしました。
ちなみに、vue createはvue-cli3.0系じゃないと使えません。

vue create .
yarn add vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-cache-inmemory graphql-tag

あなただけが使うのであればこのまま質実剛健的な画面を作り続けても良いですが、IKEAに着いてからはあなたのパートナーもこの買い物リストを見るかもしれません。
ホスピタリティと騎士道精神に溢れたあなたは軽量CSSフレームワークのMilligramをとりあえず使うことにし、index.htmlのヘッダータグの中にCDNへのリンクを追加しました。

public/index.html
<head>
  <link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">
</head>

VueとHasuraを接続する

Apolloを使ってVueとHasuraを接続します。
あなたはmain.jsに、Apolloインスタンスを使うためのコードを打ち込みます。
リンクを作成する際に指定するuriに、先ほどメモしたエンドポイントを指定します。

src/main.js
import Vue from 'vue'
import App from './App.vue'

// GraphQLを使うのに必要なライブラリをインポートする
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from "apollo-cache-inmemory";

import VueApollo from "vue-apollo";

Vue.config.productionTip = false;

// GraphQL APIへの接続先を設定する
const httpLink = new HttpLink({
  uri: 'http://localhost:8080/v1/graphql'
});

const apolloClient = new ApolloClient({
  link: httpLink,
  // 実行したクエリをメモリにキャッシュします。
  cache: new InMemoryCache(),
  connectToDevTool: true
});

Vue.use(VueApollo);

const apolloProvider = new VueApollo({
  defaultClient: apolloClient
});

new Vue({
  el: '#app',
  apolloProvider,
  render: h => h(App),
});

GraphQLでINSERTする

あなたはまず、今もまだパートナーの口から一方通行に発せられているほしい物リストを記録するため、
DBにインサートする処理から作成することにしました。
componentsディレクトリにAddTodoコンポーネントを作成します。

src/components/AddTodo.vue
<template>
    <form @submit="submit">
        <fieldset>
            <input type="text" placeholder="トゥドゥ" v-model="title">
        </fieldset>
        <input class="button-primary" type="submit" value="Send">
    </form>
</template>

<script>
import gql from 'graphql-tag';

// GraphQLクエリを作成します。INSERTなのでmutationです。
// 返り値としてINSERTしたレコードのidを指定しています。
const ADD_TODO = gql`
    mutation addTodo(
        $title: String!
    ) {
        insert_todos(
            objects: [
                {
                    title: $title
                }
            ]
        ) {
            returning {
                id
            }
        }
    }
`;

export default {
    name: "AddTodo",
    data() {
        return {title: ""};
    },
    methods: {
        // submit時にGraphQLでINSERTします。
        submit(e) {
            e.preventDefault();
            const { title } = this.$data;
            // apolloインスタンスを指定してmutateクエリを実行します。
            this.$apollo.mutate({
                mutation: ADD_TODO,
                variables: {
                    title
                },
                // INSERTした後画面をリロードしなくても良いように、TodoListで実行した取得用のクエリを自動で実行します。
                // 一度実行したクエリはメモリ上にキャッシュされていますので、クエリ名を指定します。
                refetchQueries: ["getTodos"]
            });
            this.$data.title = "";
        }
    }
}
</script>

GraphQLでGETする

せっかく追加した項目をリストとして表示できなければ仕方がありません。
あなたは買うものをリストとして表示させるTodoListと、各項目を表示させるTodoItemコンポーネントをcomponentsディレクトリに新規作成します。

src/components/TodoList.vue
<template>
    <ul>
        <TodoItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
    </ul>
</template>

<script>
import TodoItem from './TodoItem.vue';
import gql from 'graphql-tag';

// GETするだけのGrahpQLクエリです。
// idも返すように要求しますが、今回は使っていません。
const GET_TODOS = gql`
    query getTodos {
        todos {
            id
            title
        }
    }
`

export default {
    name: 'TodoList',
    components: {
        TodoItem
    },
    data() {
        return {
            todos: []
        } 
    },
    // apolloで実行するクエリを指定します。
    apollo: {
        todos: {
            query: GET_TODOS
        }
    }
}
</script>

src/components/TodoItem.vue
<template>
    <li :key="todo.id">{{ todo.title }}</li>
</template>

<script>
export default {
    name: "TodoItem",
    props: ["todo"]
}
</script>

これでINSERTとGETの実装が終わりました。
ルートコンポーネントになっているApp.vueで読み込まれているHello World的なテンプレコンポーネントを削除し、新たに作成したコンポーネントを表示させます。
あなたは雛形を作った時点でくっついているVueのロゴをどうしようかと一瞬悩みますが、マーケティングの才能も兼ね備えたあなたは、画像の1つもない殺風景な画面を見ると購買意欲が失せるだろうと考え、今回はこのまま残しておくことにしました。

src/App.vue
<template>
  <div id="app">
    <!-- 格好いいVのロゴは残す。 -->
    <img alt="Vue logo" src="./assets/logo.png">
    <add-todo/>
    <todo-list/>
  </div>
</template>

<script>
import TodoList from './components/TodoList.vue';
import AddTodo from './components/AddTodo.vue';

export default {
  name: 'app',
  components: {
    TodoList,
    AddTodo
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

あなたはここまで一通り実装を終え、実際に動かしてみることにしました。

yarn serve

エンターキーバチーン!

 DONE  Compiled successfully in 72ms                                                                                          


  App running at:
  - Local:   http://localhost:8081/

Vueアプリケーションは8081で動き始めました。

あなたは視界の隅でゆるりと動くパートナーを意識して焦燥感を覚えます。
パートナーはすでに身支度を終えつつありますが、あなたは鮮やかなパープルのパジャマを着たままです。
あなたは期待と緊張で微かに震える手でブラウザを起動し、localhost:8081に接続します。

image.png

あなたは無情にも画面幅いっぱいに広がったテキストボックスにおもむろに「イカを買う」と入力しSENDボタンをクリックします。
画面にulとして表示された文字列を見て、あなたは思いました。

「なんだかちょっと不恰好だがまあいいか」

優秀なITエンジニアであるあなたは、納期を顧みず完璧を追い求めるような愚行はおかしません。
そう。QCDです。いつだって"Done is better than perfect"なのです。
一旦このままアルファ版としてリリースします。

GraphQLでDELETEする

ここまでで買い物リストを作成することに成功しましたが、不意にあなたの頭を不安がよぎります。

魑魅魍魎の蔓延るIT業界で数多の死線をくぐり抜けてきたあなたは、既にフィックスした決定事項が偉い人の鶴の一声によっていとも簡単に覆されることを知っています。
あなたはこの買い物リストには削除機能が必要であることを直感的に予見しました。

あなたはそう思うと同時に手を動かし始め、TodoItemコンポーネントにリスト要素のクリックを取るイベントハンドラと、削除用のクエリを追加します。

src/components/TodoItem.vue
<template>
    <li :key="todo.id" v-on:click="removeTodo">{{ todo.title }}</li>
</template>

<script>
import gql from 'graphql-tag'

// GraphQLクエリを作成します。DELETEなのでmutationです。
// idが一致するレコードを削除し、返り値として削除したレコード数を返します。
const REMOVE_TODO = gql`
    mutation removeTodo(
        $id: Int!
    ) {
        delete_todos(where: {id: {_eq: $id}}) {
            affected_rows
        }
    }
`;

export default {
    name: "TodoItem",
    props: ["todo"],
    methods: {
        removeTodo() {
            const { id } = this.todo;
            this.$apollo.mutate({
                mutation: REMOVE_TODO,
                variables: {
                    id
                },
                // DELETEした後画面をリロードしなくても良いように、TodoListで実行した取得用のクエリを自動で実行します。
                // 一度実行したクエリはメモリ上にキャッシュされていますので、クエリ名を指定します。
                refetchQueries: ["getTodos"]
            });            
        }
    }
}
</script>

これでパートナーの気が変わっても安心です。
あなたはさっきの追加した「イカうんちゃら」を削除し、IKEAで買うものを追加します。

image.png

念の為Hasuraの方もブラウザで開き、DATAタブからtodosテーブルにレコードが追加されていることを確認します。

image.png

あなたはPostgresサーバーに永続化された買い物リストを確認し、静かに、しかし確かに不敵な笑みを浮かべます。
誰もが羨むほどの才能とタイムマネジメント力を併せ持つあなたは、今日もいつも通り完璧に仕事をやり遂げました。

あなたは鮮やかなパープルのパジャマを脱ぎ捨て、少しだけ光沢のある紺色のセットアップに着替えます。
あなたは完成させたばかりの買い物リストがローカルで動いているMacBookを片手に持ち、夕日の照り返しが眩しい東京湾に目を細めながら颯爽と南船橋IKEA Tokyo-Bayへと向かうのでした。

すみませんでした。
次回はAzureにデプロイしたりSubscribeしたりしたいです。

※全部フィクションです。

参考

https://hasura.io/vue-graphql
https://docs.hasura.io/1.0/graphql/manual/index.html
https://learn.hasura.io/graphql/vue/introduction
https://medium.com/@marion.schleifer/how-to-connect-your-graphql-api-to-your-vuejs-frontend-61d8e8e455db

image.png
ニンジャ!

48
33
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
48
33