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

昨日に引き続き、How To Build a Blog With Vue, GraphQL, and Apollo Clientの続きをやっていきます。多分今回が最終回になります。

過去の記事は👇になります。

# ユーザー認証

昨日はユーザー登録画面を作って、ユーザー一覧や登録されたユーザーの詳細を表示する機能を作りました。

今回は認証済みのユーザーだけPostを投稿できるようにしていきます。具体的には、Authrizationヘッダーを使ってuser tokenを渡して〜みたいな処理を入れていきます。apollo-link-context を使うと簡単にこれが実現できるとのこと。まずは src/main.js に以下のコードを追加していきます。

import { setContext } from 'apollo-link-context'

const authLink = setContext((_, { headers }) => {
    // get the authentication token from localstorage if it exists
    const token = localStorage.getItem('blog-app-token')

    // return the headers to the context so httpLink can read them
    return {
        headers: {
            ...headers,
            authorization: token ? `Bearer ${token}` : null
        }
    }
})

// update apollo client as below
const apolloClient = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache()
})

apollo-link-contexitをインポートしてから、authLinkという変数にローカルストレージから取得したuser tokenを格納して、Authorizationヘッダーにこの情報を格納します。これによってアプリケーションとGraphQLサーバーのやりとりに認証情報が付加されたことになります。

# 新しいPostの投稿

いよいよブログシステムっぽくなってきました。ブログの投稿機能です。まずはGraphQL用のミューテーションの定義をgraphql.jsに追加していきます。

export const ADD_POST_MUTATION = gql`mutation AddPostMutation($title: String!, $content: String!) {
        addPost(
            title: $title,
            content: $content
        ) {
            id
            slug
            title
            content
            user {
                id
                username
                email
            }
        }
    }`

タイトルとコンテンツを文字列として渡すだけのシンプルなもの。GraphQLサーバーを通じてデータベースに保存されます。

そして、Adminフォルダの中にAddPostコンポーネントを作っていきます。

<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">Add Post</h2>

                    <form method="post" @submit.prevent="addPost">
                        <div class="field">
                            <label class="label">Title</label>

                            <p class="control">
                                <input
                                    class="input"
                                    v-model="title"
                                    placeholder="Post title">
                            </p>
                        </div>

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

                            <p class="control">
                                <textarea
                                    class="textarea"
                                    rows="10"
                                    v-model="content"
                                    placeholder="Post content"
                                    ></textarea>
                            </p>
                        </div>

                        <p class="control">
                            <button class="button is-primary">Add Post</button>
                        </p>
                    </form>
                </div>
            </div>
        </div>
    </section>
</template>

<script>
import Menu from '@/components/Admin/Menu'
import { ADD_POST_MUTATION, ALL_POSTS_QUERY } from '@/graphql'

export default {
    name: 'AddPost',
    components: {
        Menu
    },
    data () {
        return {
            title: '',
            content: ''
        }
    },
    methods: {
        addPost () {
            this.$apollo
                .mutate({
                    mutation: ADD_POST_MUTATION,
                    variables: {
                        title: this.title,
                        content: this.content
                    },
                    update: (store, { data: { addPost } }) => {
                        // read data from cache for this query
                        const data = store.readQuery({ query: ALL_POSTS_QUERY })

                        // add new post from the mutation to existing posts
                        data.allPosts.push(addPost)

                        // write data back to the cache
                        store.writeQuery({ query: ALL_POSTS_QUERY, data })
                    }
                })
                .then(response => {
                    // redirect to all posts
                    this.$router.replace('/admin/posts')
                })
        }
    }
}
</script>

mutateの中がちょっとややこしいですが、まず必要な値を渡すところからはじまるけど、Apolloクライアントがローカル側にキャッシュをするので、そのキャッシュも更新してあげなきゃいけないっていう流れ。最初に全部のPostをALL_POSTS_QUERYで持ってきて、それからallPostsという配列に全部詰め込んで、最終的にそれをキャッシュにもどしてやる的な。そして正しくポストされたら一覧画面に飛ばしてやる(ココは後ほど実装。Userの場合と似てますね)

# PostをRouteに追加

src/router/index.jsを開いて👇を追加していきます。

import AddPost from '@/components/Admin/AddPost'

// add these inside the `routes` array
{
    path: '/admin/posts/new',
    name: 'AddPost',
    component: AddPost
}

で、またいつものように要らないセミコロンを無くしたり、ファイルの最後に改行いれたりして、文法エラーを取り除いてから👇のように。

post

# Postの表示

続いてGraphQLクエリを使って投稿を取ってきて表示します。 graphql.jsに👇を追加してから、

export const ALL_POSTS_QUERY = gql`query AllPostsQuery {
        allPosts {
            id
            title
            slug
            user {
                username
            }
        }
    }`

AdminフォルダにPosts.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">Posts</h2>

                    <table class="table is-striped is-narrow is-hoverable is-fullwidth">
                        <thead>
                            <tr>
                                <th>title</th>
                                <th>User</th>
                                <th></th>
                                </tr>
                        </thead>
                        <tbody>
                            <tr
                                v-for="post in allPosts"
                                :key="post.id">
                                    <td>{{ post.title }}</td>
                                    <td>{{ post.user.username }}</td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </section>
</template>

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

export default {
    name: 'Posts',
    components: {
        Menu
    },
    data () {
        return {
            allPosts: []
        }
    },
    apollo: {
        // fetch all posts
        allPosts: {
            query: ALL_POSTS_QUERY
        }
    }
}
</script>

Menuコンポーネントをここでも使いつつ、、って👇の記述、Usersのをコピペしてきて、そのままPostsに変えようと思ってたけど、変更するの忘れっちゃってた系じゃないですか??笑

posts

まぁ、でも、このPosts.vueは余計なセミコロンとか無いし、コピペしただけで大丈夫そうだったのとやってることはApollo clientを使って全ての投稿を取ってきてるだけなので、大丈夫かな?ってことで先に進みます。

# Postsをルートに追加

また流れ作業的に作ったPostsコンポーネントをインポートして、パスの情報を追加します。

import Posts from '@/components/Admin/Posts'

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

そしてようやく気づいてきたんだけど、これ、VS Codeでフォーマットすると、かっこの前のスペースが無くなったり、シングルクォートがダブルクォートになったり、不要なセミコロンが付くんじゃぁ、、、っていう。 👇みたいなエラーを見かけたら無条件にフォーマットしてしまった自分が至らなかっただけなのか、、と。。。mm

Expected indentation of 2 spaces but found 4  

んま、そんなこんなで /admin/posts/new でブログの投稿を行って、

add

Postの一覧をみると👇狙ったように見えています。

list

# BlogのHomepageを作る

投稿されたポストが一覧になっているページということで、Postsを表示するのと同じGraphQLクエリをつかいますが、Homeコンポーネントとしてsrc/components配下にHome.vueを作ってやっていきます。

<template>
    <section class="section">
        <div class="columns">
            <div class="column is-6 is-offset-3">
                <h1 class="title">Latest Posts</h1>

                <h3
                    v-for="post in allPosts"
                    :key="post.id"
                    class="title is-5">
                        <router-link :to="post.slug">
                            {{ post.title }}
                        </router-link>
                </h3>
            </div>
        </div>
    </section>
</template>

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

export default {
    name: 'Home',
    data () {
        return {
            allPosts: []
        }
    },
    apollo: {
        // fetch all posts
        allPosts: {
            query: ALL_POSTS_QUERY
        }
    }
}
</script>

# Homeをルートに登録

👇のように

import Home from '@/components/Home'

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

今度はインデントだけを直したので、セミコロンとかダブルクォーテーションがどうのとかは言われませんでした 😃

home

# Postを表示

最後にホーム画面に表示された各ポストがクリックされた時のコンポーネントを作っていきます。

まずは src/graphql.js に以下のクエリを追加して

export const POST_QUERY = gql`query PostQuery($slug: String!) {
        post(slug: $slug) {
            id
            title
            slug
            content
            user {
                id
                username
                email
            }
        }
    }`

SinglePost.vueというコンポーネントを追加して(4タブを2タブにして)

<template>
    <section class="section">
        <div class="columns">
            <div class="column is-6 is-offset-3">
                <router-link class="button is-link is-small" to="/">Back Home</router-link>

                <h1 class="title">
                    {{ post.title }}
                </h1>

                <div class="content">
                    {{ post.content }}
                </div>
            </div>
        </div>
    </section>
</template>

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

export default {
    name: 'SinglePost',
    data () {
        return {
            post: '',
            slug: this.$route.params.slug
        }
    },
    apollo: {
        // fetch post by slug
        post: {
            query: POST_QUERY,
            variables () {
                return {
                    slug: this.slug
                }
            }
        }
    }
}
</script>

src/router/index.js のRouteに追加してやると、、

import SinglePost from '@/components/SinglePost'

// add these inside the `routes` array
{
    path: '/:slug',
    name: 'SinglePost',
    component: SinglePost,
    props: true
}

👇無事にPostが表示されました!

single

# まとめ

このチュートリアルではGraphQL, Apollo client, そしてVue.jsを使ってブログサービスを構築してきました。その中でフロントエンドアプリからGraphQLサーバーへの接続も体験しました。

完成版のコードは GitHub にあります。

ということで、 Chimezie Enyinnaya さん、ありがとうございました!楽しかったし勉強になりました!


GraphQLを使ってこうやってサクサク開発できると楽しいですね〜。 んま、逆に複雑なシステムでGraphQLサーバー側がしんどくなりそうなのは想像できそうなところではありますが。。

そして、大体コピペでVue.jsを使ったモノづくりは何となく手応えが掴めてきたので、ぼちぼちVue.jsというかJavaScriptのお勉強をしていきたいものです(遠い目)

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

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