尝试使用HeadLessCMS + GraphQL + Vue.js来创建SPA (博客)

我希望学习一些新技术,于是我用HeadLessCMS + GraphQL + Vue.js创建了一个简单的SPA(博客)。
在这个过程中,我将所学的内容整理成了教程形式。
为了让初学者更容易理解,我尽量多地附上了屏幕截图。

总结

使用Headless CMS、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

制作一个样品

我将创建一个具有两种类型页面(帖子列表和详细页面)的博客(Single Page Application)。外观方面,我会尽力使用Vue的组件库Vuetify来实现类似的效果。

output1.gif

你可以在这里确认已经部署在Heroku上的内容。
(由于不会一直运行,所以初次访问可能会有一些延迟)
https://graphcms-sampleblog.herokuapp.com/

这里是源代码。
https://github.com/kawamataryo/vue-sample-blog

创建API服务器

首先,使用 headressCMS 的 GraphCMS 创建 API 服务器。
正如其名,只需在浏览器中点点点就能创建基础设施、数据库建设和 API 设计等等,无需考虑其他问题,
就能创建 API 服务器。而且,响应格式是 GraphQL。

创建GraphCMS账户

スクリーンショット 2018-09-16 15.37.30.png
スクリーンショット 2018-09-16 15.37.44.png

创建模式

我們將根據側邊欄選單的方案來創建模型。這就像是一個數據庫,與API的數據有關聯。
在模型中,您可以像WordPress的自定義文章類型一樣創建輸入字段。

スクリーンショット 2018-09-16 15.39.25.png

可以通过右上角的“CreateModel”进行创建。
本次是以以下内容进行创建的。

# モデルの構成
displayName: post
api key: Post
# フィールドの構成
title: single line text
description: multi type text
content: mark down
thumbnail: asset picker
スクリーンショット 2018-09-16 15.40.54.png

添加数据

您可以选择侧边栏中的“内容”>“帖子”,然后使用右上角的“创建帖子”来添加数据。
让我们随便添加一些内容。
后面将使用vue-apollo来获取并解释这些数据。

スクリーンショット 2018-09-16 20.47.14.png

尝试使用GraphQL

输入内容后,立即尝试使用GraphQL。可以在侧边栏的API Explorer中尝试发送GraphQL请求。输入时也会有自动补全功能,点击右侧的Docs按钮,可以查看与创建的模型相匹配的GraphQL查询信息。

スクリーンショット 2018-09-17 23.35.30.png

确认API信息

可以从侧边栏的设置菜单中确认用于数据获取的端点。
因为是GraphQL,所以只需一行代码。
此外,您还可以在设置菜单中进行端点的公开设置等操作。
初始设置为只读。本次操作无需特别更改。

スクリーンショット 2018-09-16 15.41.49.png

前端开发

接下来我们将使用graphCMS创建的数据来创建一个前端界面来显示它。我们将使用可以简单创建vue项目的vue cli工具。

创建项目

首先,安装Vue CLI。


$ npm install -g @vue/cli-service-global

然后使用Vue CLI 3创建项目。


$ vue create sample-blog

如果选择默认选项,可以。
通过以下命令启动,并在访问http://localhost:8088/后成功启动时,项目创建成功。


$ cd sample-blog
$ yarn serve
スクリーンショット 2018-09-23 7.06.43.png

添加Vuetify

因为即使是模拟也能提高观感,所以我们首先添加了vue.js的组件库vuetify。添加了它之后,可以使用各种材料设计的组件。vuetify也有丰富的手册,部分内容已经翻译成日语。

安装

在Vue CLI3中,可以很容易地将Vuetify作为插件添加进去。

$ 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已成功安装完成。

スクリーンショット 2018-09-23 7.27.58.png

如果在运行 “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")

包含了.browserslistrc和package.json,这两个文件都存在可能是问题的原因,所以我删除了.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”的命令并且显示如下内容,那就可以了。
之后,我们将根据这个组件进行数据的添加等操作。

スクリーンショット 2018-09-23 8.05.12.png

在中国,添加API客户端Apollo

Apollo是一个GraphQL客户端库。
我们将使用Apollo从HeadlessCMS获取数据来尝试一下。

安装和配置vue-apollo。

安装使用Vue CLI。
其他安装方法请参考这里的说明。

$  vue add apollo       

所有选项都可以使用默认设置。
vue-apollo.js配置文件会自动创建在src/下,
并且会在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文件的端点设置栏,

// 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 cli 3文档中的此处。

创建.env文件

# プロジェクトルートで、、
$ vi .env

请向.env文件中添加以下内容。graphCMS的终端点请使用之前提及的设置中的内容进行复制。

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 来编写查询。
最开始是获取所有帖子的操作。

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 中添加的帖子数据。

eoutput2.gif

添加路由 vue-router

因为SPA(单页应用)的需要,我们将在一览页面上添加到详细页面的路由。
我们将使用vue-router来实现路由功能。

安装和设置

为了避免使用vue cli的add命令时默认添加文件以及自动更改App.vue文件导致的麻烦,建议使用yarn来安装vue-router。

$ 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>

到目前为止,路由器的设置已经完成。

增加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上显示以下详细信息,那就可以了。

スクリーンショット 2018-09-24 0.13.09.png

使用Apollo从详细页面获取数据。

我們將之前直接寫在組件中的post改為使用Apollo進行獲取。

首先,在graphql.js中添加一个查询函数,用于通过ID获取一篇文章的数据。

.
.
// 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() 来设置要传递给查询的参数。本例中,我们使用 this.$route.params.id 将 Vue Router 的 URL 参数设置为 id。您可以获取到 localhost:8080/post/xxx 中的 xxx。

在列表中添加到详细信息的链接。

我们将在列表的最后添加一个链接到详细信息。在vuetify组件中,您可以使用:to来设置vue-router链接。
我们将在帖子卡片组件中的列表中添加一个链接。

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

以上是路由完成的部分。
接下来应该能够按照以下的方式进行屏幕跳转。

output3.gif

目前为止前端部分已经完成。
虽然我本来还想实现分页等功能,但是因为已经变得太长了,所以我想另外找个机会整理一下。

部署

最后,既然已经做好了这个项目,我们就试着进行外部公开吧。
通过使用Heroku,你可以轻松地部署Vue项目。

表达的添加和设置

添加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中添加服务器启动命令等。

  "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命令行工具的设置,继续进行。

Heroku使用Git来进行部署。
首先,我们需要编辑.gitignore文件,以便将dist/目录下的可执行文件纳入版本控制。

$ vi .gitignore

让我们注释或删除/dist的部分。

# /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的导航栏和页脚等,就可以创建一个演示网站。

印象

哎呀,Vue.js真好玩。速度感完全不同。GraphQL太厉害了。查询非常易懂。
我真的觉得编码、设计、部署、发布的循环真的很快。
真的非常感谢那些创造了这么棒的开源软件的前辈们。

希望在接下来的文章中将无法解释的分页和CRUD(添加、编辑、删除文章)内容公开。如果发现拼写错误或者不推荐的写法,请通过编辑请求或评论告知,不胜感激。

追加记录

我添加了有关实现分页的文章。
尝试使用vue-apollo + vuetify实现分页(存在问题)。

广告
将在 10 秒后关闭
bannerAds