# 『その2』 Vue.jsとGraphQLとApollo Clientでブログを作るチュートリアル

前回から少し間が空いてしまいましたが、How To Build a Blog With Vue, GraphQL, and Apollo Clientの続きをやっていきたいと思います。

前回(その1)でははsqliteとか入れて環境を作ってVue.jsのプロジェクトを作ってからコンポーネントの登録などをして、Bulma CSSを導入したところで終わっています。

# 不要なコードの削除

今回はHelloコンポーネントは使わないので、その削除とindex.jsからの参照を無くしましょう。 ってか、HelloというかHelloWorldですかねぇ👇

Hello

ってことで、index.jsでもインポートしないようにしてnew Routerの中からも削除しました。

# マスターレイアウトの追加

src/App.vueで共通のレイアウトを定義してそれを使っていきましょう、と。templateのappというdivの中にnavを追加していきます。

<template>
  <div id="app">
    <nav class="navbar is-primary" role="navigation" aria-label="main navigation">
      <div class="container">
        <div class="navbar-brand">
          <router-link class="navbar-item" to="/">Blog App</router-link>

          <button class="button navbar-burger">
            <span></span>
            <span></span>
            <span></span>
          </button>
        </div>
      </div>
    </nav>
    <router-view />
  </div>
</template>

# ユーザーのサインアップ

ユーザーはサインアップをしてAdmin系の作業をするとのことで、src/componentsにAdminというフォルダーを作ってそこにコンポーネントを配置していきます。

また、SignUpコンポーネントを作って行く前に、GraphQLクエリとミューテーションを行うgraphql.jsというファイルをsrcの中に作ります。👇のようにユーザ名とメールアドレスとパスワードでサインアップする感じ。

import gql from 'graphql-tag'

export const SIGNUP_MUTATION = gql`mutation SignupMutation($username: String!, $email: String!, $password: String!) {
        createUser(
            username: $username,
            email: $email,
            password: $password
        ) {
            id
            username
            email
        }
    }`

で、上記のusername, email, passwordはSignUpコンポーネントを通して渡されます、と。

ということで、SignUpコンポーネントを作っていきましょう。Adminフォルダーの中にSignUp.vueを作っていきます。

👇ちょっと長いけど、やってることはusename, email, passwordそれぞれのインプットがあって、SignUpボタンが押されると、GraqhQLのmutationが走ってログイン画面に戻へ〜という流れ。

<template>
    <section class="section">
        <div class="columns">
            <div class="column is-4 is-offset-4">
                <h2 class="title has-text-centered">Signup</h2>

                <form method="POST" @submit.prevent="signup">
                    <div class="field">
                        <label class="label">Username</label>

                        <p class="control">
                            <input
                                type="text"
                                class="input"
                                v-model="username">
                        </p>
                    </div>

                    <div class="field">
                        <label class="label">E-Mail Address</label>

                        <p class="control">
                            <input
                                type="email"
                                class="input"
                                v-model="email">
                        </p>
                    </div>

                    <div class="field">
                        <label class="label">Password</label>

                        <p class="control">
                            <input
                                type="password"
                                class="input"
                                v-model="password">
                        </p>
                    </div>

                    <p class="control">
                        <button class="button is-primary is-fullwidth is-uppercase">SignUp</button>
                    </p>
                </form>
            </div>
        </div>
    </section>
</template>

<script>
import { SIGNUP_MUTATION } from '@/graphql'

export default {
    name: 'SignUp',
    data () {
        return {
            username: '',
            email: '',
            password: ''
        }
    },
    methods: {
        signup () {
            this.$apollo
                .mutate({
                    mutation: SIGNUP_MUTATION,
                    variables: {
                        username: this.username,
                        email: this.email,
                        password: this.password
                    }
                })
                .then(response => {
                    // redirect to login page
                    this.$router.replace('/login')
                })
            }
        }
    }
</script>

上記のSIGNUP_MUTATIONは、先程graphql.jsで定義したもの。

# SignUpをRouteに追加

src/router/index.jsに👇のコードを追加してSignUpコンポーネントの登録を行います。

import SignUp from '@/components/Admin/SignUp'

// add these inside the `routes` array
{
    path: '/signup',
    name: 'SignUp',
    component: SignUp
},

ってことで、npm run devしてサーバー起動しようとしたら、めっちゃ怒られた…w functionの名前と()の間にスペース空けろとか、ダブルクォーテーションじゃなくてシングルクォーテーションとか、セミコロンを取り除きなさいとか、、"オレが書いたコードじゃなくてコピペしただけだし…"とか思ったりもしたけど、丁寧に一つ一つエラーを取り除いていきました 😃

Error

ということで、Signupページが表示されました👇

Signup

# ログイン機能

続いてログイン機能を作っていきましょう。先ほど作ったgraphql.jsでSIGNUP_MUTATIONの下にLOGIN_MUTATIONを作成します。

export const LOGIN_MUTATION = gql`mutation LoginMutation($email: String!, $password: String!) {
        login(
            email: $email,
            password: $password
        )
    }`

今回はメールアドレスとパスワードでログインする形。

続いてAdminフォルダーの中にLogIn.vueというファイルを作っていきます。

<template>
    <section class="section">
        <div class="columns">
            <div class="column is-4 is-offset-4">
                <h2 class="title has-text-centered">Login</h2>

                <form method="POST" @submit.prevent="login">
                    <div class="field">
                        <label class="label">E-Mail Address</label>

                        <p class="control">
                            <input
                                type="email"
                                class="input"
                                v-model="email">
                        </p>
                    </div>

                    <div class="field">
                        <label class="label">Password</label>

                        <p class="control">
                            <input
                                type="password"
                                class="input"
                                v-model="password">
                        </p>
                    </div>

                    <p class="control">
                        <button class="button is-primary is-fullwidth is-uppercase">Login</button>
                    </p>
                </form>
            </div>
        </div>
    </section>
</template>

<script>
import { LOGIN_MUTATION } from '@/graphql'

export default {
    name: 'LogIn',
    data () {
        return {
            email: '',
            password: ''
        }
    },
    methods: {
        login () {
            this.$apollo
                .mutate({
                    mutation: LOGIN_MUTATION,
                    variables: {
                        email: this.email,
                        password: this.password
                    }
                })
                .then(response => {
                    // save user token to localstorage
                    localStorage.setItem('blog-app-token', response.data.login)

                    // redirect user
                    this.$router.replace('/admin/posts')
                })
        }
    }
}
</script>

# RouteにLoginを追加

index.jsのRouterの配列に👇を追加

import LogIn from '@/components/Admin/LogIn'

// add these inside the `routes` array
{
    path: '/login',
    name: 'LogIn',
    component: LogIn
}

そして、また文法エラーが出ている箇所をチマチマ直して、、と。。

でもって👇Login画面が出せましたよ、と。

Login

管理系のメニューを作っていきます。Adminフォルダーの中にMain.vueを。

<template>
    <aside class="menu">
        <p class="menu-label">Post</p>
        <ul class="menu-list">
            <li>
            <router-link to="/admin/posts/new">New Post</router-link>
            </li>
            <li>
            <router-link to="/admin/posts">Posts</router-link>
            </li>
        </ul>
        <p class="menu-label">User</p>
        <ul class="menu-list">
            <li>
                <router-link to="/admin/users">Users</router-link>
            </li>
        </ul>
    </aside>
</template>

# ユーザーを表示

管理系セクションとしてユーザーをリスト表示する機能を作っていきます。Usersコンポーネントを作って、GraphQLでユーザー一覧を取得します。

まずはgraphql.jsに👇を追加。

export const ALL_USERS_QUERY = gql`query AllUsersQuery {
        allUsers {
            id
            username
            email
        }
    }`

続いてAdminフォルダの下にUsers.vueを作って👇こんな感じで。

<template>
    <section class="section">
        <div class="container">
            <div class="columns">
                <div class="column is-3">
                    <Menu/>
                </div>
                <div class="column is-9">
                    <h2 class="title">Users</h2>

                    <table class="table is-striped is-narrow is-hoverable is-fullwidth">
                        <thead>
                            <tr>
                                <th>Username</th>
                                <th>Email</th>
                                <th></th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr
                                v-for="user in allUsers"
                                :key="user.id">
                                    <td>{{ user.username }}</td>
                                    <td>{{ user.email }}</td>
                                    <td>
                                        <router-link :to="`/admin/users/${user.id}`">View</router-link>
                                    </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </section>
</template>

<script>
import Menu from '@/components/Admin/Menu'
import { ALL_USERS_QUERY } from '@/graphql'

export default {
    name: 'Users',
    components: {
        Menu
    },
    data () {
        return {
            allUsers: []
        }
    },
    apollo: {
        // fetch all users
        allUsers: {
            query: ALL_USERS_QUERY
        }
    }
}
</script>

Memuコンポーネントを活用しつつ、GraphQLサーバーからデータを取得します。apolloオブジェクトの中で全てのユーザーを取得するのにALL_USERS_QUERYを使っていくのに大切なのはGraphQLで使われるallUsersとdataの中の名前(allUsers)を合わせておくことでございます。で、取得したデータをテーブル形式で表示しますよ、と。

# UsersをRouteへ追加

でもって、またsrc/router/index.jsに👇を追加していきます。そろそろ流れが掴めてきた感。

import Users from '@/components/Admin/Users'

// add these inside the `routes` array
{
    path: '/admin/users',
    name: 'Users',
    component: Users
},

またチマチマ文法エラーをアレしてから、、

あれ、、なんかエラー出てるすね…w

undefined

が、サーバー再起動したらエラー出なくなった、、けど、今度はSignUpのところで👇ってエラーが出てる。。

[Vue warn]: Error in v-on handler: "TypeError: Cannot read property 'defaultClient' of undefined"

これって、main.jsの👇ここのapploClientが上手いこといってないってことですよね、、

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

ってことで、わっかんないけどSignUp.vueの$apolloの後に👇ガッツリ明示してみたんだけど、、

this.$apollo.provider.defaultClient

今度はクライアント側でNetwork errorが出てて、GraphQLサーバー側みてみたら👇こんなエラーが出てました

Self referencing config has been depreciated. We recommend to you manually define the value for app.appKey
Learn more at https://adonisjs.svbtle.com/depreciating-self-reference-inside-config-files
Self referencing config has been depreciated. We recommend to you manually define the value for app.appKey
Learn more at https://adonisjs.svbtle.com/depreciating-self-reference-inside-config-files
(node:51022) UnhandledPromiseRejectionWarning: RuntimeException: E_MISSING_APP_KEY: Make sure to define appKey inside config/app.js file before using Encryption provider
    at Function.missingAppKey (/Users/eijishinohara/vueis/adonis-graphql-server/node_modules/@adonisjs/generic-exceptions/src/RuntimeException.js:50:12)
    at new Encryption (/Users/eijishinohara/vueis/adonis-graphql-server/node_modules/@adonisjs/framework/src/Encryption/index.js:35:33)
    at Object.closure (/Users/eijishinohara/vueis/adonis-graphql-server/node_modules/@adonisjs/framework/providers/AppProvider.js:240:14)
    at Ioc._resolveBinding (/Users/eijishinohara/vueis/adonis-graphql-server/node_modules/@adonisjs/fold/src/Ioc/index.js:231:68)
    at Ioc.make (/Users/eijishinohara/vueis/adonis-graphql-server/node_modules/@adonisjs/fold/src/Ioc/index.js:807:19)
    at /Users/eijishinohara/vueis/adonis-graphql-server/node_modules/@adonisjs/fold/src/Ioc/index.js:318:19
    at Array.map (<anonymous>)
    at Ioc._makeInstanceOf (/Users/eijishinohara/vueis/adonis-graphql-server/node_modules/@adonisjs/fold/src/Ioc/index.js:317:44)
    at Ioc.make (/Users/eijishinohara/vueis/adonis-graphql-server/node_modules/@adonisjs/fold/src/Ioc/index.js:799:19)
    at AuthManager.getScheme (/Users/eijishinohara/vueis/adonis-graphql-server/node_modules/@adonisjs/auth/src/Auth/Manager.js:82:16)
(node:51022) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 11)

なんかKeyが無いとか言ってますね。。ググっていくと、RuntimeException: E_MISSING_APP_KEY: Make sure to define appKey inside config/app.js file before using Encryption provider #1とかってのが出てきて、👇すればイイって書いてあるけど。。

adonis key:generate

ってことで、、

$ adonis key:generate
generated: unique APP_KEY

もっかいGraphQLサーバー立ち上げ直して、、

$ adonis serve --dev

 SERVER STARTED 
> Watching files for changes...

2020-05-08T08:08:46.091Z - info: serving app on http://127.0.0.1:3333

signupからサブミットしたらログイン画面に飛ぶようになりました。からの〜 admin/users に行ったらちゃんと値入ってた。

users

でもって .provider.defaultClient を取り除いて👇に戻してもちゃんと動いてたのでメデタシメデタシ。

this.$apollo

なんというか👇こんな感じでシマシマになってるんですね〜

users

# Userの詳細

👆でユーザーごとにViewってあるけど、まぁまだ実装してねっすよねっていうところで、、graphql.jsに新しいクエリを追加していきます。

export const USER_QUERY = gql`query UserQuery($id: Int!) {
        user(id: $id) {
            id
            username
            email
            posts {
                id
            }
        }
    }`

続いてUserDetails.vueをAdminフォルダの下に。

<template>
    <section class="section">
        <div class="container">
            <div class="columns">
                <div class="column is-3">
                    <Menu/>
                </div>
                <div class="column is-9">
                    <h2 class="title">User Details</h2>

                    <div class="field is-horizontal">
                        <div class="field-label is-normal">
                            <label class="label">Username</label>
                        </div>
                        <div class="field-body">
                            <div class="field">
                                <p class="control">
                                    <input class="input is-static" :value="user.username" readonly>
                                </p>
                            </div>
                        </div>
                    </div>

                    <div class="field is-horizontal">
                        <div class="field-label is-normal">
                            <label class="label">Email Address</label>
                        </div>
                        <div class="field-body">
                            <div class="field">
                                <p class="control">
                                    <input class="input is-static" :value="user.email" readonly>
                                </p>
                            </div>
                        </div>
                    </div>

                    <div class="field is-horizontal">
                        <div class="field-label is-normal">
                            <label class="label">Number of posts</label>
                        </div>
                        <div class="field-body">
                            <div class="field">
                                <p class="control">
                                    <input class="input is-static" :value="user.posts.length" readonly>
                                </p>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>
</template>

<script>
import Menu from '@/components/Admin/Menu'
import { USER_QUERY } from '@/graphql'

export default {
    name: 'UserDetails',
    components: {
        Menu
    },
    data () {
        return {
            user: '',
            id: this.$route.params.id
        }
    },
    apollo: {
        // fetch user by ID
        user: {
            query: USER_QUERY,
            variables () {
                return {
                    id: this.id
                }
            }
        }
    }
}
</script>

ユーザーのidを使ってクエリを投げた結果を画面に表示する感じです。Viewリンクは👇こんな感じになってるので渡ってきたidを使える、と。

<router-link :to="`/admin/users/${user.id}`">View</router-link>

# ユーザーの詳細をRouteに追加

段々流れ作業的になってきましたw

import UserDetails from '@/components/Admin/UserDetails'

// add these inside the `routes` array
{
    path: '/admin/users/:id',
    name: 'UserDetails',
    component: UserDetails,
    props: true
},

でもって、また文法エラーをアレして…w

👇あれっ、、画面出たけど、なんかlengthがundefinedってエラー出てるな、、

details

今日のところはこのエラーを取り除いて終わりにしてあげよう…w

👇のところでユーザーがまだ何も投稿をしていなかったらエラーが出てしまうようなので、 v-if="user.posts" を付けてあげたら、エラーでなくなりました〜

<input class="input is-static" :value="user.posts.length" readonly />

ってことで、今回はこの辺で終了。

このエントリーをはてなブックマークに追加

Algolia検索からの流入のみConversionボタン表示