# 『その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ですかねぇ👇
ってことで、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の名前と()の間にスペース空けろとか、ダブルクォーテーションじゃなくてシングルクォーテーションとか、セミコロンを取り除きなさいとか、、"オレが書いたコードじゃなくてコピペしただけだし…"とか思ったりもしたけど、丁寧に一つ一つエラーを取り除いていきました 😃
ということで、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画面が出せましたよ、と。
# Menuコンポーネントの作成
管理系のメニューを作っていきます。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
が、サーバー再起動したらエラー出なくなった、、けど、今度は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 に行ったらちゃんと値入ってた。
でもって .provider.defaultClient を取り除いて👇に戻してもちゃんと動いてたのでメデタシメデタシ。
this.$apollo
なんというか👇こんな感じでシマシマになってるんですね〜
# 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ってエラー出てるな、、
今日のところはこのエラーを取り除いて終わりにしてあげよう…w
👇のところでユーザーがまだ何も投稿をしていなかったらエラーが出てしまうようなので、 v-if="user.posts" を付けてあげたら、エラーでなくなりました〜
<input class="input is-static" :value="user.posts.length" readonly />
ってことで、今回はこの辺で終了。