新しめの技術を勉強したいと思い、HeadLessCMS + GraphQL + Vue.js で簡単なSPA(ブログ)を作ってみました。
その過程で学んだことをチュートリアル形式でまとめます。
なるべく初学者の方にもわかりやすいようスクリーンショットを多めに掲載しています。
概要
headlessCMS、GraphQL、Vue.js、Apolloでブログを作成します。
チュートリアルがメインなので、graphQLのクエリの詳細な説明などは省略しています。
以下各バージョンです。
- GraphCMS: 2018.7公開のもの
- Vue cli: 3.0.1
- vue: 2.5.17
- vue-apollo: 3.0.0-beta.11
- vue-router": 3.0.1
- vuetify: 1.2.4
- graphql: 14.0.2
作成サンプル
このようなのトップページ(投稿一覧)と詳細ページの2種類のページを持つブログ(SPA)を作成します。
見栄えはvueのコンポーネントライブラリのVuetifyでそれっぽく頑張っています。
herokuにデプロイしているものはこちらから確認できます。
(常に起こしているわけではないので、初回アクセスは起動が遅いです)
https://graphcms-sampleblog.herokuapp.com/
ソースはこちらです。
https://github.com/kawamataryo/vue-sample-blog
APIサーバーの作成
まず、headressCMSのGraphCMSを使ってAPIサーバーを作成します。
CMSという名の通り、プラウザでポチポチとやっていたらインフラ、DBの構築、APIの設計等々なにも考える必要なく、
APIサーバーをつくることが出来ます。しかもレスポンスはGraphQLです。
GraphCMSアカウント作成
GraphCMSホームページからSignUpでアカウントを作成します。
github、facebook等でのsignupも出来ます。
次にプロジェクトを作成します。
今回は、sampleというプロジェクトを作成しました。
スキーマの作成
サイドメニューのschemeよりModelを作成します。
DBのようなものです。これがAPIのデータにひも付きます。
Model内にwordpressのカスタム投稿タイプのように、inputフィールドを作成することができます。
右上のCreateModelより作成できます。
今回は以下内容で作成しました。
# モデルの構成
displayName: post
api key: Post
# フィールドの構成
title: single line text
description: multi type text
content: mark down
thumbnail: asset picker
データの追加
サイドメニューのcontent > posts を選択し、右上のcreate Postよりデータの追加が行えます。
なにか適当に追加しておきましょう。
こちらのデータを後から説明するvue-apolloで取得します。
GraphQLを試してみる
コンテンツを入力したら、早速GraphQLを試してみましょう。
サイドメニューのAPI Exploereより、GraphQLのリクエストが試せます。
入力のオートコンプリートも効きますし、右側のDocsという小さいボタンをクリックすると、
作成したModelに合わせた、GraphQLのquery情報が確認できます。
API情報の確認
データの取得に使用するエンドポイントは、サイドメニューのsettingsより確認できます。
GraphQLなので、ワンラインです。
その他、settignsではエンドポイントの公開設定等行えます。
初期はreadOnlyです。今回は特に変更せずとも大丈夫です。
フロントエンド開発
つづいてgraphCMSで作成したデータを表示するフロントエンドを作成します。
vueプロジェクトを簡単に作成できるvue cliを使用します。
プロジェクトの作成
まず、vue cliのインストール。
$ npm install -g @vue/cli-service-global
そしてvue cli3でプロジェクトを作成します。
$ vue create sample-blog
選択肢はデフォルトでOKです。
以下コマンドで起動し、
http://localhost:8088/にアクセスして無事起動できればプロジェクト作成OKです。
$ cd sample-blog
$ yarn serve
vuetifyの追加
モックでも見栄えが良いほうがやる気がでるので、最初にvue.jsのコンポーネントライブラリのvuetifyを追加します。
追加するとマテリアルデザインの様々なコンポーネントが使えるようになります。
vuetifyはマニュアルも豊富で、一部日本語翻訳されています。
インストール
vue cli3 ではvuetifyをpluginとして簡単に追加できます。
$ vue add vuetify
するとオプションの選択肢が出てきます。
今回は以下のように設定しました。
? Use a pre-made template? (will replace App.vue and HelloWorld.vue) Yes
? Use custom theme? No
? Use custom properties (CSS variables)? No
? Select icon font fa4
? Use fonts as a dependency (for Electron or offline)? No
? Use a-la-carte components? No
? Use babel/polyfill? Yes
? Select locale en
それでは以下コマンドで起動してみます。
$ yarn serve
http://localhost:8080/
にアクセスして、無事下記画面が表示されれば、vuetifyのインストール完了です。
補足 yarn serveでエラーが発生した場合
私の環境だと以下エラーが発生しました。
ERROR Failed to compile with 1 errors 07:22:42
error in ./src/main.js
Module build failed (from ./node_modules/babel-loader/lib/index.js):
BrowserslistError: [BABEL] /Users/kawamataryou/vue_training/sample-blog/src/main.js: /Users/kawamataryou/vue_training/sample-blog contains both .browserslistrc and package.json with brow
sers (While processing: "/Users/kawamataryou/vue_training/sample-blog/node_modules/@vue/babel-preset-app/index.js$0")
contains both .browserslistrc and package.json with browとの通り、package.jsonと、.browserslistrcが両方あることが問題のようなので、.browserslistrcを削除します。
$ rm .browserslistrc
そして再起動すれば解消されました。
サンプルコンポーネントを作成
早速vuetifyを使って記事一覧表示用のCardコンポーネントを作成します。
$ touch src/components/PostCard.vue
src/components/PostCard.vue に以下を記載します。
<template>
<v-flex xs12 sm6 md4>
<article>
<v-hover>
<v-card
slot-scope="{ hover }"
:class="`elevation-${hover ? 12 : 2}`"
>
<a href="">
<v-img
class="white--text"
height="170px"
:src=post.thumbnail.url
>
</v-img>
</a>
<v-card-title>
<div>
<h2>{{ post.title }}</h2>
<span class="grey--text">{{ post.createdAt}}</span><br>
<span>{{ post.description }}</span>
</div>
</v-card-title>
<v-card-actions>
<v-btn icon class="red--text">
<v-icon medium>fa-reddit</v-icon>
</v-btn>
<v-btn icon class="light-blue--text">
<v-icon medium>fa-twitter</v-icon>
</v-btn>
<v-btn icon class="blue--text text--darken-4">
<v-icon medium>fa-facebook</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn flat color="blue" href="">Read more</v-btn>
</v-card-actions>
</v-card>
</v-hover>
</article>
</v-flex>
</template>
<script>
export default {
name: "PostCard",
props: ["post"]
}
</script>
そして、このコンポーネントを読み込むようにApp.vueを修正します。
<template>
<v-app>
<v-container grid-list-xl>
<v-layout row wrap>
<PostCard
v-for="post in posts"
v-bind:key="post.id"
:post=post
></PostCard>
</v-layout>
</v-container>
</v-app>
</template>
<script>
import PostCard from './components/PostCard'
export default {
name: 'App',
components: {
PostCard,
},
data: () => ({
posts: [
{
id: 1,
createdAt: "2018/09/23",
title: "sample post",
description: "sample post description",
contents: "sample post contents",
thumbnail: {
url: "https://picsum.photos/800/400?image=80"
}
},
{
id: 2,
createdAt: "2018/09/24",
title: "sample post2",
description: "sample post description2",
contents: "sample post contents2",
thumbnail: {
url: "https://picsum.photos/800/400?image=90"
}
},
{
id: 3,
createdAt: "2018/09/24",
title: "sample post3",
description: "sample post description3",
contents: "sample post contents3",
thumbnail: {
url: "https://picsum.photos/800/400?image=100"
}
},
]
})
}
</script>
この時点でyarn serveを行い、以下のように表示されればOKです。
以降このコンポーネントをもとにデータの追加などを行っていきます。
APIクライアント Apolloの追加
ApolloはGraphQLのクライントライブラリです。
Apolloを使ってHeadlessCMSからデータを取得してみます。
vue-apolloのインストールと設定
インストールはvue cliを使います。
その他のインストール方法はこちらで説明されています。
$ vue add apollo
オプションはすべてデフォルトで大丈夫です。
自動的にsrc/以下にapolloの設定ファイルvue-apollo.jsが作成され、
main.jsに読み込み設定が追加されます。
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
// Install the vue plugin
Vue.use(VueApollo)
// Name of the localStorage item
const AUTH_TOKEN = 'apollo-token'
// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/graphql'
// Config
const defaultOptions = {
// You can use `https` for secure connection (recommended in production)
httpEndpoint,
// You can use `wss` for secure connection (recommended in production)
// Use `null` to disable subscriptions
wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
// LocalStorage token
tokenName: AUTH_TOKEN,
// Enable Automatic Query persisting with Apollo Engine
persisting: false,
// Use websockets for everything (no HTTP)
// You need to pass a `wsEndpoint` for this to work
websocketsOnly: false,
// Is being rendered on the server?
ssr: false,
.
.
.
import '@babel/polyfill'
import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
+ import { createProvider } from './vue-apollo'
Vue.config.productionTip = false
new Vue({
+ apolloProvider: createProvider(),
render: h => h(App)
}).$mount('#app')
エンドポイントの設定を追加で行います。
src/vue-apollo.jsをのEndpointの設定欄を見ると、、
// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/graphql'
となっています。これは、エンドポイントとして環境変数のVUE_APP_GRAPHQL_HTTPを参照し、
それが設定されて無ければ'http://localhost:4000/graphql'をエンドポイントとするという意味です。
なので、vue cliの環境変数にVUE_APP_GRAPHQL_HTTPを追加して、そこにGraphCMSのエンドポイントURLを設定しましょう。
また、wsEndpointについては使用しないので、nullを設定します。
環境変数の設定についてはvue clid3ドキュメントのこちらに記載されています。
.envファイルを作成
# プロジェクトルートで、、
$ vi .env
.envに以下を追記します。graphCMSのエンドポイントは前述のsettingsからコピーしたものを使ってください。
VUE_APP_GRAPHQL_HTTP=https://api-apeast.graphcms.com/xxxxxxxxx/master
そしてwssは今回使用しないので、
以下オプションはapollo.jsの以下オプションは削除してください。
.
.
const defaultOptions = {
// You can use `https` for secure connection (recommended in production)
httpEndpoint,
- // You can use `wss` for secure connection (recommended in production)
- // Use `null` to disable subscriptions
- wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
// LocalStorage token
tokenName: AUTH_TOKEN,
.
.
これでApolloの設定は終了です。
GraphQLの追加
やっとGraphQLの出番です。ここから、GraphQLのクエリを書いていきます。
まず、クエリを記述するgraphql.jsファイルの追加です。
$ mkdir src/constants
$ touch src/constants/graphql.js
graphql.jsにクエリを書いていきます。
最初はpostの全件取得です。
import gql from 'graphql-tag'
// すべての投稿を取得
export const ALL_POSTS = gql`
query allPosts {
posts {
id
title
content
description
createdAt
thumbnail {
url
}
}
}
`
作成したクエリをApp.vueで読み込みます。
そして、dataで直書きしていた部分をApolloでの取得結果に置き換えます。
apollo: 以下に先程宣言したクエリを追加するだけです。
.
.
.
<script>
import PostCard from './components/PostCard'
+ import {ALL_POSTS} from "./constants/graphql";
export default {
name: 'App',
components: {
PostCard,
},
+ apollo: {
+ posts: ALL_POSTS
+ }
- data: () => ({
- posts: [
- {
- id: 1,
- createdAt: "2018/09/23",
- title: "sample post",
- description: "sample post description",
- contents: "sample post contents",
- thumbnail: {
- url: "https://picsum.photos/800/400?image=80"
- }
- },
- {
- id: 2,
- createdAt: "2018/09/24",
- title: "sample post2",
- description: "sample post description2",
- contents: "sample post contents2",
- thumbnail: {
- url: "https://picsum.photos/800/400?image=90"
- }
- },
- {
- id: 3,
- createdAt: "2018/09/24",
- title: "sample post3",
- description: "sample post description3",
- contents: "sample post contents3",
- thumbnail: {
- url: "https://picsum.photos/800/400?image=100"
- }
- },
- ]
- })
}
</script>
これで、yarn serveで起動するとGraphCMSで追加した投稿データが表示されるはずです。
ルーティングの追加 vue-router
SPAということで、一覧ページから詳細ページへのルーティングを追加します。
ルーティングにはvue-routerを使用します。
インストールと設定
vue-routerは、yarnでインストールします。
(vue cliのaddだとデフォルトファイルの追加や、App.vueの書き換えが自動で行われ面倒なので、、)
$ yarn add vue-router
次に設定ファイルを追加します。
$ touch src/router.js
router.jsにルーティングを記述します。
ここで宣言してるPostList.vueとPostDetail.vueは後ほど追加します。
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
// ルーティング
const routes = [
{
// 記事一覧ページ
path: '/',
name: "postList",
component: () => import('./views/PostList.vue')
},
{
// 記事詳細ページ
path: '/post/:id',
name: "postDetail",
component: () => import('./views/PostDetail.vue')
},
]
const router = new VueRouter({
routes: routes,
base: process.env.BASE_URL,
mode: 'history',
// ページ遷移の際の位置指定 指定がない場合 ページトップへ
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return {x: 0, y: 0}
}
}
});
export default router;
次にこれをmain.jsで読み込むよう設定を追記します。
import '@babel/polyfill'
import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import { createProvider } from './vue-apollo'
+ import router from "./router"
Vue.config.productionTip = false
new Vue({
apolloProvider: createProvider(),
+ router,
render: h => h(App)
}).$mount('#app')
最後に、App.vueの一覧ページのコンポーネントを記載していた部分をvue-routerの遷移タグ に変更します。
<template>
<v-app>
<transition name="fade">
<router-view/>
</transition>
</v-app>
</template>
<script>
export default {
name: 'App',
}
</script>
ここまででrouterの設定は完了です。
vueコンポーネントの追加
次にページ遷移先のvueコンポーネントを設定していきます。
一覧ページと詳細ページのコンポーネントを作成します。
一覧ページは、App.vueに記載していたものを移して作成します。
<template>
<v-container grid-list-xl>
<v-layout row wrap>
<PostCard
v-for="post in posts"
v-bind:key="post.id"
:post=post
></PostCard>
</v-layout>
</v-container>
</template>
<script>
import PostCard from '../components/PostCard'
import {ALL_POSTS} from "../constants/graphql";
export default {
name: "PostList",
components: {
PostCard,
},
data: () => ({
posts: [],
}),
apollo: {
posts: ALL_POSTS
}
}
</script>
詳細ページは、以下のような構成にします。
まだ、postがdataに直書きですが、後からapolloでの取得に切り替えます。
<template>
<div>
<section>
<v-parallax :src="post.thumbnail.url" height="600">
<v-layout
column
align-center
justify-center
class="white--text"
>
<h1 class="white--text mb-2 display-2 text-xs-center">{{post.title}}</h1>
<p class="white-text text-xs-center subheading">{{ post.description }}</p>
</v-layout>
</v-parallax>
</section>
<v-content>
<v-container>
<v-layout row wrap justify-center>
<v-flex xs12 md8>
<p>{{ post.content }}</p>
</v-flex>
</v-layout>
</v-container>
</v-content>
</div>
</template>
<script>
export default {
name: 'PostDetail',
data: () => ({
post: {
title: "sample title",
description: "sample description",
content: "amet, consectetur adipisicing elit. Culpa debitis dolores eius facilis officiis quo unde velit. Ab accusantium aperiam commodi cupiditate dignissimos eius eum sequi sunt tempore, vero voluptas.Consequuntur deleniti doloremque eius incidunt modi non repellendus sapiente ut vel. Autem neque, ullam. Atque aut eveniet, exercitationem illo illum inventore molestias numquam, optio quas recusandae, repellendus suscipit tenetur vero?Blanditiis consequuntur deserunt dolor ducimus modi necessitatibus, odit placeat quaerat quia saepe sequi unde ut voluptate? Aspernatur consectetur, dignissimos eaque fuga in laborum odit, quibusdam rem rerum sed sit soluta?Aliquam atque deleniti dolorem laborum maxime voluptates? A architecto, corporis earum explicabo fugiat labore porro velit! Adipisci at consequatur, error eveniet, laboriosam minus nemo nihil numquam quisquam rerum sequi temporibus.Deleniti, fuga, quibusdam! Aspernatur commodi cum doloremque esse est eum illo inventore ipsum itaque laborum molestias mollitia nostrum, odio officiis omnis perspiciatis possimus quia quidem recusandae tempore vero voluptate voluptates.",
thumbnail: {
url: "https://picsum.photos/1200/600?image=90"
}
}
})
}
</script>
<style>
.v-parallax__image {
opacity: 1!important;
}
</style>
http://localhost:8080/ で今までの一覧ページ。
http://localhost:8080/post/1 で以下詳細ページが表示されればOKです。
詳細ページのデータをApolloで取得
前項でコンポーネントに直書きしていたpostをApolloでの取得に切り替えます。
まず、IDで記事を1件取得するクエリをgraphql.jsに追記します。
.
.
// IDで1件取得
export const FEACH_POST_BY_ID = gql`
query feachPostById($id: ID!) {
post(where: { id: $id }) {
title
content
description
createdAt
thumbnail {
url
}
}
}
`
次にそれを、PostDetailで読み込みます。
以下を修正します。
<script>
import {FEACH_POST_BY_ID} from "../components/graphql";
export default {
name: 'PostDetail',
data: () => ({
post: []
}),
apollo: {
post: {
query: FEACH_POST_BY_ID,
variables() {
return {
id: this.$route.params.id,
}
},
}
}
}
</script>
apollo.post.variables() 以下で、クエリに渡す引数を設定できます。
今回はidとして、this.$route.params.idでvue-routerのurlパラーメーターを設定しています。
localhost:8080/post/xxxのxxxの部分が取得できます。
一覧から詳細へのリンクの追加
最後に一覧から詳細へのリンクを設定します。vuetifyのコンポーネントでは:toでvue-routerのリンクを設定可能です。
一覧のPostCardコンポーネントにリンクを追加します。
<v-flex xs12 sm6 md4>
<article>
<v-hover>
<v-card
slot-scope="{ hover }"
:class="`elevation-${hover ? 12 : 2}`"
>
+ <a v-bind:href="'/post/' + post.id">
<v-img
class="white--text"
height="170px"
:src=post.thumbnail.url
>
</v-img>
+ </a>
<v-card-title>
<div>
<h2>{{ post.title }}</h2>
<span class="grey--text">{{ post.createdAt | moment }}</span><br>
<span>{{ post.description }}</span>
</div>
</v-card-title>
<v-card-actions>
<v-btn icon class="red--text">
<v-icon medium>fa-reddit</v-icon>
</v-btn>
<v-btn icon class="light-blue--text">
<v-icon medium>fa-twitter</v-icon>
</v-btn>
<v-btn icon class="blue--text text--darken-4">
<v-icon medium>fa-facebook</v-icon>
</v-btn>
<v-spacer></v-spacer>
- <v-btn flat color="blue">Read more</v-btn>
+ <v-btn flat color="blue" :to="'/post/' + post.id">Read more</v-btn>
</v-card-actions>
</v-card>
</v-hover>
</article>
</v-flex>
</template>
以上でルーティングが完成です。
以下のように画面遷移ができるはずです。
ここまででフロントエンドは終了です。
本当はpagingなどもやりたいのですが、かなり長文となってしまっているので、別の機会にまとめたいと思います。
デプロイ
最後に、せっかく作ったプロジェクトなので外部公開してみましょう。
herokuを使えば簡単にvueプロジェクトがデプロイできます。
expressの追加と設定
Node.jsのサーバーサイドJavascriptの実行環境 explessを追加します。
$ yarn add express
そして、サーバーの設定となるserver.jsをプロジェクトルートに追加します。
$ vi server.js
const express = require('express');
const port = process.env.PORT || 5000;
const app = express();
app.use(express.static(__dirname + "/dist/"));
app.get(/.*/, function(req, res) {
res.sendfile(__dirname + "/dist/index.html");
});
app.listen(port);
server.jsの書き方については、以下を参考にさせていただきました。
Vue Cli 3のプロジェクトをHerokuにデプロイする
次にpackage.jsonにserverの起動コマンド等を追加します。
"name": "sample-blog",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
+ "postinstall": "yarn build",
+ "start": "node server.js"
},
.
.
.
この状態で、
$ yarn start
でエラーなくサーバーが起動し http://localhost:5000/ にアクセスして一覧画面が表示されればサーバーの設定は完了です。
helokuへのデプロイ
heloku cliの設定は終わっている前提で進めます。
herokuはgitを介してデプロイを行います。
まず実行ファイルとなるdist/以下のディレクトリがバージョン管理されるように.gitignoreを編集します。
$ vi .gitignore
/distの欄をコメントアウトor削除しましょう。
# /dist
次にgitの初期化とステージング、commitを行います。
$ git init
$ git add .
$ git commit -m "init for heroku"
次にherokuのプロジェクトを作成します。
$ heroku create graphcms-sampleblog
# graphcms-sampleblogの部分がurlの一部となります。既存と被ると使えないので注意
そして、heroku上にデプロイします。
コマンド一つでデプロイ完了です。
$ git push heroku master
早速サイトを見てみましょう。
$ heroku open
https://プロジェクト名.herokuapp.com/ で無事プラウザで表示されればデプロイ完了です。
あと、vuetifyのnavbarや、footerなどを最初にサンプルで掲載した追加すればデモサイトが出来ます。
感想
いやー。Vue.js楽しい。スピード感が全く違う。GraphQLすごい。クエリがわかりやすい。
コーディング、デザイン、デプロイ、公開までのサイクルが本当に早いと感じました。
こんな素晴らしいOSSを作ってくれている先人に感謝です。
今回、説明できなかったページングや、CRUD(記事追加、編集、削除)についても今後記事を公開したいと思います。
typo、この書き方は非推奨、など情報ありましたら編集リクエストかコメントで教えて頂ければ幸いです。
追記
ページネーションの実装について記事追加しました。
vue-apollo + vuetifyでページネーションを実装してみる(課題あり)