Vue.js, GraphQLで Craft CMS にエントリを投稿する #craftcms

このエントリは Craft CMS Advent Calendar 2019 14日目のエントリーです。
昨日は @tinybeans の『カラフルボックスで始める - はじめての Craft CMS (3) 「Craft CMS のインストール」 』でした。


先日のエントリと近いところはあるのだけど。

Craft CMS へ GraphQL で投げるサンプルっぽいものを Vue.js で。
PWA対応させてアプリっぽくスマホに登録しておければいつでも使えるのでは?というのをお試しでやってみた。

といっても基本的には「vue.js, Apollo, graphQLで名簿アプリを作成する(CRUD機能の実装サンプル)」を読みながら真似させてもらった。

vue.js, Apollo, graphQLで名簿アプリを作成する(CRUD機能の実装サンプル) - Qiita
https://qiita.com/ryo2132/item...

Vue.js のプロジェクトを作成

まずは Vue.js のプロジェクトを作成する

$ vue create postcraftvue

$ vue add vuetify

$ vue add apollo

選択肢は全てデフォルトで。
apollo がいい感じに GraphQL を扱ってくれる。

.env を作成して、エンドポイントとToken を追加しておく。

VUE_APP_GRAPHQL_HTTP=https://example.com/api
VUE_APP_GRAPHQL_APIKEY=hogehoge

Craft CMS 側の CORS 対応

ローカルでの開発時や作ったものは別のところにアップすると思うので、Craft CMS とはドメインが変わる。

なので CORS 対応として craftql.php を Craft 側(craft/config 以下)に用意する。

<?php

return [
    'allowedOrigins' => [
        'http://localhost:8080', // or '*', as Tim mentions
        'https://example.com', 
    ]
];

こんな感じで、 http:// からかいてポート番号も入れておいてローカルで問題なく動いた。

CORS Error · Issue #51 · markhuot/craftql · GitHub
https://github.com/markhuot/cr...

データ取得の確認

Mutation する前にとりあえずデータが取れているのを確認する。

src/constants/test.js を用意してこんな感じで用意する。

import gql from 'graphql-tag'

// すべてのお知らせを取得
export const ALL_CUSTOMERS = gql`
  query{
    customers: entries( section:[news], type:[News]) {
      id
      title
      slug
      postDate
      ...on News{
        c_textarea01
      }
    }
  }
`

CraftQL と Craft 自体での Query の書き方が微妙に違っていた。
入力タイプでのフィルタも追加しておいた。

Craft 自体の場合

entries( section:"news")

Craft QL の場合

entries(section:[news])

src/components/test.vue を用意する。

変数名とかはとりあえず参考エントリのままつかったので custmer とかのまま。

<template>
  <v-container>
    <!--ツールバー-->
    <v-toolbar flat color="grey lighten-2">
      <v-toolbar-title>テスト</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn color="primary" dark class="mb-2" @click="showDialogNew">新規追加</v-btn>
    </v-toolbar>

    <!-- データテーブル -->
    <v-data-table
        :headers="headers"
        :items="customers"
        no-data-text="エントリがありません。"
        class="elevation-1"
    >
      <v-progress-linear slot="progress" color="blue" indeterminate></v-progress-linear>
      <template slot="items" slot-scope="props">
        <td>{{ props.item.id }}</td>
        <td>{{ props.item.title }}</td>
      </template>
    </v-data-table>
  </v-container>
</template>

<script>
  import {ALL_CUSTOMERS} from "../constants/test";

  export default {
    name: "CustomerTable",
    data: () => ({
      // 顧客情報
      customers: [],
      // テーブルのヘッダー情報
      headers: [
        {text: 'ID', value: 'id'},
        {text: 'タイトル', value: 'title'},
      ],
    }),
    apollo: {
      // すべての顧客情報の取得
      customers: ALL_CUSTOMERS
    }
  }
</script>

App.vue をこんな感じにしてとりあえずデータが取れているのを確認。

<template>
  <v-app>
    <CustomerTable/>
  </v-app>
</template>

<script>
import CustomerTable from './components/test'

export default {
  name: 'App',
  components: {
    CustomerTable
  },
}
</script>

エントリの作成

取得ができたので作成を試していく。

参考記事だと、更新・削除、もあるのだけどとりあえずは新規作成のみ。
変更は管理画面でやればいいかなぁというかんじで。

src/constants/news-mutation.js を作成する。

内容としてはこんな感じで。

import gql from 'graphql-tag'

export const CREATE_CUSTOMER = gql`
  mutation createNewEntry($title:String, $c_textarea01:String) {
    upsertNews(
      title:$title,
      c_textarea01:$c_textarea01,
    ){
      id
      title
      slug
      postDate
      ...on News{
        c_textarea01
      }
    }
  }
`

createNewEntry にしてそこに入力した値を渡すようにした。

参考記事は投げるfieldのしか書いていないのだけど、id とかレスポンスの方も書いておかないとうまいこと行かなかったのでそうしておいた。

nystudio107 | Using VueJS + GraphQL to make Practical Magic
https://nystudio107.com/blog/u...

test.vue を post.vue に複製してこんな感じで入力フォームを追加した。

最初は参考記事を参考にしてモーダルで動くのを確認してから、ベタなフォームに変更。

<template>
  <v-container>

  <!--入力フォーム-->
    <v-card>
      <v-container>
        <h2>Create Entry</h2>
        <v-form ref="form" v-model="valid" lazy-validation>
          <!--タイトル-->
          <v-text-field
              v-model="customer.title"
              :rules="nameRules"
              label="タイトル"
              required
          ></v-text-field>
          <!--本文-->
          <v-textarea
              v-model="customer.c_textarea01"
              label="本文"
          ></v-textarea>
          <v-btn
              :disabled="!valid"
              @click="createNewEntry"
          >
            追加
          </v-btn>
          <v-btn @click="clear">クリア</v-btn>
        </v-form>
      </v-container>
    </v-card>

    <!--ツールバー-->
    <v-toolbar flat color="grey lighten-2">
      <v-toolbar-title>エントリー一覧</v-toolbar-title>
    </v-toolbar>

    <!-- データテーブル -->
    <v-data-table
        :headers="headers"
        :items="customers"
        no-data-text="エントリーがありません。"
        class="elevation-1"
    >
      <v-progress-linear slot="progress" color="blue" indeterminate></v-progress-linear>
      <template slot="items" slot-scope="props">
        <td>{{ props.item.id }}</td>
        <td>{{ props.item.title }}</td>
        <td>{{ props.item.c_textarea01 }}</td>
        <td>{{ props.item.postDate | dateFormatter }}</td>
      </template>
    </v-data-table>
  </v-container>
</template>

<script>
  import {ALL_CUSTOMERS} from "../constants/test";
  import {CREATE_CUSTOMER} from "../constants/news-mutation";

  export default {
    name: "CustomerTable",
    data: () => ({
      // 顧客情報
      customers: [],
      // テーブルのヘッダー情報
      headers: [
        {text: 'ID', value: 'id'},
        {text: 'タイトル', value: 'title'},
        {text: '本文', value: 'c_textarea01'},
        {text: '投稿日', value: 'postDate'},
      ],
      // データテーブルの初期ソート、表示件数の設定
      // pagination: {
      //   descending: true,
      //   rowsPerPage: 10
      // },
      customer: {
        id: '',
        title: '',
        c_textarea01: '',
      },
      // バリデーション
      valid: true,
      nameRules: [
        v => !!v || '名前は必須項目です',
        v => (v && v.length <= 20) || '名前は20文字以内で入力してください'
      ],
      // ローディングの表示フラグ
      progress: false,
      // ダイアログの表示フラグ
      dialog: false,
      // 新規・更新のフラグ
      isCreate: true,
    }),
    apollo: {
      // すべての顧客情報の取得
      customers: ALL_CUSTOMERS
    },
    methods: {
      // --------------------------------
      // 新規作成
      // --------------------------------
      createNewEntry: function () {
        if (this.$refs.form.validate()) {
          this.progress = true
          this.$apollo.mutate({
            mutation: CREATE_CUSTOMER,
            variables: {
              title: this.customer.title,
              c_textarea01: this.customer.c_textarea01,
            }
          }).then(() => {
            this.$apollo.queries.customers.fetchMore({
              updateQuery: (previousResult, {fetchMoreResult}) => {
                return {
                  customers: fetchMoreResult.customers
                }
              }
            })
            this.dialog = false
            this.progress = false
          })
        }
      },
      // --------------------------------
      // フォームのクリア
      // --------------------------------
      clear: function () {
        this.$refs.form.reset()
      },
      // --------------------------------
      // 新規追加ダイアログの表示
      // --------------------------------
      // showDialogNew: function () {
      //   // this.clear()
      //   this.isCreate = true
      //   this.dialog = true
      // }
    }
  }
</script>

script に methods などを追加する。

さっき createNewEntry にしたので methods に追加するのも createNewEntry で。

ひとまずこれでローカルで表側からの投稿も問題なくできた。

投稿されるとリストにもすぐ反映される。

heroku へデプロイ

ここまではローカルでやってたけど、どこかにdeployして登録するところまで持っていければって感じで引き続き。

以前やった heroku へのデプロイで試してみる。

Node.jsの実行環境 express をインストールする。

$ yarn add express

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

package.json にこんな感じで追記した。

"scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
+    "postinstall": "yarn build",
+    "start": "node server.js"

その後で

$ yarn build

$ yarn start

で、問題なく http://localhost:5000/ で確認できた。

Git周りの設定と heroku の設定。

// git 設定
$ git init
$ git add .
$ git commit -m "init for heroku"

// プロジェクトを作成
$ heroku create hogehoge

// heroku へdeploy
$ git push heroku master

// サイトをみる
$ heroku open

でデプロイ先でも確認できた。

PWA 対応する

続いてPWA対応をしていく。

Vue.jsをPWA化してService WorkerにRuntime Cacheを実装、そして公開するまで - Qiita
https://qiita.com/kawakawaryur...

にあるような感じで

$ yarn add @vue/cli-plugin-pwa —dev
$ git add .
$ git commit -m "add @vue/cli-plugin-pwa"
$ vue add pwa
$ git push heroku master

で、 Mac の Chrome でみてもインストールリンクが表示された。

iOS でも Safari でホーム画面に追加をして、そこから投稿ができた。

Mac からもこんな感じで投稿前

投稿後

とりあえずやりたいことはできたからよかったよかった。

非常に参考になる先人の方々のエントリなどのおかげ。感謝感謝。


これだけだと投げ先が固定になっているので汎用的に使えなかったり、オフラインの時どうするか?といった課題はある。

投げるためにはネットワークないと使えないから、オフライン時は一旦データ貯めておくとかそういう感じになるのかなー。

画像周りをどうしたものか、というのもある。

CraftQL の Roadmap みるとこんな感じなので、まだできないのかな。。。

https://github.com/markhuot/cr...

とりあえずは動く箱ができただけで今回は満足。

vue.js周りをもっと勉強しよう。