概要

Webフレームワークを使った開発といえば、Webフレームワークが持つテンプレートエンジンでHTMLを生成して返す、という流れが基本でしたが、昨今のWebアプリケーションはバックエンド側はRESTful APIとして作成し、フロントエンドはVue.jsやAngular、ReactなどのJavaScript製フレームワークで別途作成し、JSONでやり取りする、というようにバックエンドとフロントエンドを分けた状態で作成する事が多くなりました。

さて、GrailsといえばApache Groovy用のフルスタックWebフレームワークです。
今まではGrailsがリクエストを受けたら、最終的にGrailsのテンプレートであるGSPファイルを処理してHTMLをクライアントに返していました。
しかし、GrailsもGrails3からGSPを介さずに、単純にJSONをクライアントに返す、ということがより簡単に実現できるようになりました。
さらに、今回紹介するVue.js用のプロファイルを利用してアプリケーションを作成することで、

  • 自動的にVue.js用のプロジェクトが作成される
  • Node.jsやYarn、NPMと言ったフロントエンド開発用のツールをインストールする必要はない
  • 当然Grailsのフル機能が利用可能

と言った素晴らしい環境を手に入れることが出来ます。
フロントエンドは色々開発ツールが合って大変そう。。。と思っていたサーバサイドエンジニアの方の強い味方になるのでは無いでしょうか!?

アプリケーションの作成

今回試した環境は以下のとおりです。
Grails: 3.3.3
Java: 8u152-zulu
アプリケーション名: gvue

Vue.js用のプロファイルでアプリケーションを作りたい場合は、以下のような書式になります。

grails create-app アプリケーション名 --profile=vue

では実際に作成します。

[koji:grails]$ grails create-app gvue --profile=vue
Resolving dependencies....
| Application created at /home/koji/work/grails/gvue
| This profile provides a client/server multi-project build structure. The server Grails app is using the rest-api profile with CORS enabled. It can be started using 'grails run-app' or using the Gradle wrapper:

  ./gradlew server:bootRun

The Vue client app has been built via the Vue CLI (https://github.com/vuejs/vue-cli), using the webpack template. It can be started via 'npm start' (in which case you will need to run 'npm install' to install npm dependencies) or using the Gradle wrapper (which will install npm dependencies automatically if needed):

  ./gradlew client:start

For support, please use the Grails Community Slack (https://grails-slack.cfapps.io) or open an issue on Github: https://github.com/grails-profiles/vue/issues

[koji:grails]$

プロジェクトディレクトリ

さて、上記のコマンドを実行すると、gvueというディレクトリが作成され、その中身は以下のようになっています。

[koji:gvue]$ ls -ahl
合計 36K
drwxr-xr-x 5 koji koji 4,0K Mär 13 09:48 .
drwxr-xr-x 4 koji koji 4,0K Mär 13 09:48 ..
drwxr-xr-x 7 koji koji 4,0K Mär 13 09:48 client
drwxr-xr-x 3 koji koji 4,0K Mär 13 09:48 gradle
-rwxr--r-- 1 koji koji 4,9K Mär 13 09:48 gradlew
-rwxr--r-- 1 koji koji 2,3K Mär 13 09:48 gradlew.bat
drwxr-xr-x 4 koji koji 4,0K Mär 13 09:48 server
-rw-r--r-- 1 koji koji   26 Mär 13 09:48 settings.gradle
[koji:gvue]$

serverディレクトリ

これがGrailsのコードを配置する場所です。
中身は以下のとおりです。

[koji:gvue]$ ls -l server 
合計 36
-rw-r--r--  1 koji koji 2387 Mär 13 09:48 build.gradle
-rw-r--r--  1 koji koji   45 Mär 13 09:48 gradle.properties
drwxr-xr-x 10 koji koji 4096 Mär 13 09:48 grails-app
-rw-r--r--  1 koji koji 5463 Mär 13 09:48 grails-wrapper.jar
-rwxr--r--  1 koji koji 4672 Mär 13 09:48 grailsw
-rwxr--r--  1 koji koji 2375 Mär 13 09:48 grailsw.bat
drwxr-xr-x  5 koji koji 4096 Mär 13 09:48 src
[koji:gvue]$

rest-apiプロファイルで作成されたGrailsアプリケーションと同様のもので、デフォルトでCORSも全てのホストに対して有効になっています。

clientディレクトリ

こちらがVue.jsのファイル群を設置するディレクトリです。
中身は以下のようになっています。

[koji:gvue]$ ls -l client 
合計 36
-rw-r--r-- 1 koji koji  549 Mär 13 09:48 README.md
drwxr-xr-x 2 koji koji 4096 Mär 13 09:48 build
-rw-r--r-- 1 koji koji 1120 Mär 13 09:48 build.gradle
drwxr-xr-x 2 koji koji 4096 Mär 13 09:48 config
-rw-r--r-- 1 koji koji  355 Mär 13 09:48 index.html
-rw-r--r-- 1 koji koji 2711 Mär 13 09:48 package.json
drwxr-xr-x 5 koji koji 4096 Mär 13 09:48 src
drwxr-xr-x 2 koji koji 4096 Mär 13 09:48 static
drwxr-xr-x 4 koji koji 4096 Mär 13 09:48 test
[koji:gvue]$

こちらはvue-cliwebpackプロファイルで作成されたものと同様のものになっています。

起動

プロジェクトのトップディレクトリにgradlewファイルがあるのでそれを実行するだけでOKです!

[koji:gvue]$ ./gradlew bootRun -parallel

初回実行時には必要なライブラリなどがダウンロードされたりするので時間がかかります。しばし待ちます。

最終的に以下のようなメッセージが表示されれば完了です。

 I  Your application is running here: http://localhost:3000
Grails application running at http://localhost:8080 in environment: development
>

これで、フロントエンドはhttp://localhost:3000/#/でアクセスでき、バックエンドはhttp://localhost:8080/でアクセスできる状態になっています。

JavaScriptの開発環境を自前で整える必要は無く、全てGrailsが面倒を見てくれます!非常に楽ちんですね!

なお、上記で./gradlew bootRun -parallelを使ってバックエンドとフロントエンドを同時に起動しましたが、以下のようにそれぞれ別個に起動することも出来ます。

# サーバ(Grails)のみ起動
./gradlew server:bootRun

# クライアント(Vue.js)のみ起動)
./gradlew client:start

RESTful APIサーバを作ってみる

では、BookとAuthorというリソースを管理するアプリケーションを実装してみます。
ココでいうリソースは、Grailsのドメインに相当します。

リソースクラスの作成(ドメイン)

Grailsでの作業になるので、serverディレクトリに移動してからgrailsコマンドを実行します。

[koji:gvue]$ pwd
/home/koji/work/grails/gvue
[koji:gvue]$ cd server 
[koji:gvue]$ grails

これで現在Grailsのインタラクティブモードに入っています。
では、以下のコマンドを実行してBookというドメインを作成します。

grails> create-domain-resource Book
| Created grails-app/domain/gvue/Book.groovy
| Created src/test/groovy/gvue/BookSpec.groovy
grails> create-domain-resource Author
| Created grails-app/domain/gvue/Author.groovy
| Created src/test/groovy/gvue/AuthorSpec.groovy
grails>

デフォルトでBook.groovyAuthor.groovyがそれぞれ以下のように作成されます。

package gvue


import grails.rest.*

@Resource(readOnly = false, formats = ['json', 'xml'])
class Book {

}
package gvue


import grails.rest.*

@Resource(readOnly = false, formats = ['json', 'xml'])
class Author {

}

これらを以下のように修正します。

package gvue


import grails.rest.*

@Resource(readOnly = false, formats = ['json', 'xml'])
class Book {
    String title
}
package gvue


import grails.rest.*

@Resource(readOnly = false, formats = ['json', 'xml'])
class Author {
    String name
    static hasMany = [books: Book]
}

このアプリケーションはAuthorとBookというリソースを管理していて、Authorは0冊以上のBookを持っている、という内容になります。
なんと!サーバ側のRESTful APIの実装はこれだけで完了です!
さらに!Grailsによってこのリソースは自動的にデータベースに永続化されるようになっています!(デフォルトではインメモリのH2データベースなので、Grailsの再起動の度に初期化されます。)

では、もしすでにアプリケーションが実行されているようでしたら、ctrl cで一旦停止してから、再度アプリケーションを起動してください。

[koji:gvue]$ ./gradlew bootRun -parallel

データを取得してみる

ではcurlで実際にAPIを叩いてみましょう。

[koji:~]$ curl http://localhost:8080/book
[]%

[koji:~]$ curl http://localhost:8080/author
[]%
[koji:~]$

ちゃんと値(今は空っぽ)が返って来ましたね!
さらにGrailsはデフォルトでちゃんとJSONとして値を返してくれています。

[koji:~]$ curl -D - http://localhost:8080/book
HTTP/1.1 200 
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Mon, 19 Mar 2018 11:55:28 GMT

[]%
[koji:~]$

データを追加してみる

では値を追加してみます。
追加なので当然POSTですね。
これも単純にcurlで送るだけでOKです。

[koji:~]$ curl -H 'Content-Type:application/json' -d '{"title": "book 1"}' http://localhost:8080/book
{"id":1,"title":"book 1"}%
[koji:~]$ 
[koji:~]$ curl -H 'Content-Type:application/json' -d '{"title": "book 2"}' http://localhost:8080/book
{"id":2,"title":"book 2"}%
[koji:~]$

では実際に登録されたかどうか確認してみます。

[koji:~]$ curl http://localhost:8080/book
[{"id":1,"title":"book 1"},{"id":2,"title":"book 2"}]%
[koji:~]$ 
[koji:~]$ curl http://localhost:8080/book/1
{"id":1,"title":"book 1"}% 
[koji:~]$

完璧です!
なお、Grailsのドメインクラスは、特に指定がない限り全てのプロパティは必須入力になっています。
そのため、もしnameプロパティが指定されていないデータが送られてきた時には、ちゃんとエラーを返してくれます。

[koji:~]$ curl -H 'Content-Type:application/json' -d '{}' http://localhost:8080/book 
{"message":"[class gvue.Book]クラスのプロパティ[title]にnullは許可されません。","path":"/book/index","_links":{"self":{"href":"http://localhost:8080/book/index"}}}%
[koji:~]$

次に、Bookをプロパティに持つAuthorの方もリソースを作成しておきます。

[koji:~]$ curl -X POST -H 'Content-Type:application/json' -d '{ "name": "KOJI", "books": [{"id": 1}, {"id": 2}] }' http://localhost:8080/author
{"id":1,"books":[{"id":1},{"id":2}],"name":"KOJI"}%

値を確認すると

[koji:~]$ curl http://localhost:8080/author
[{"id":1,"books":[{"id":2},{"id":1}],"name":"KOJI"}]%

となっています。完璧ですね!
なお、idというプロパティはGrailsが自動的に各ドメインに割り当てて連番として扱ってくれる特殊なプロパティです。

RESTful APIクライアントを作ってみる

ではクライアント側を作ってみましょう。
以下はGrailsは全く関係ない、VueとVueRouterの話になります。

コンポーネントの作成

まずgvue/client/src/Book.vueを作成します。

<template>
  <div>
    <h1>Books</h1>
    <input type="text" value="" v-model="title">
    <button @click="add">add</button>
    <ul>
      <li v-for="book in books" :key="book.id">{{book.title}}({{book.id}})</li>
    </ul>
  </div>
</template>
<script>
export default {
  name: 'Book',
  data () {
    return {
      title: '',
      books: []
    }
  },
  methods: {
    add: function () {
      const headers = new Headers()
      headers.append('Accept', 'application/json')
      headers.append('Content-Type', 'application/json')
      fetch(`${process.env.SERVER_URL}/book`, {
        method: 'POST',
        headers: headers,
        body: JSON.stringify({title: this.title})
      })
        .then(response => response.json())
        .then(json => {
          const added = json
          console.log(added)
          this.books.push(added)
        })
        .catch(err => {
          alert('Something wrong')
          console.log(err)
        })
    }
  },
  created: function () {
    fetch(`${process.env.SERVER_URL}/book`)
      .then(response => response.json())
      .then(json => {
        this.books = json
      })
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

ルートの追加

client/src/router/index.jsを以下のように修正します。

import Vue from 'vue'
import Router from 'vue-router'
import Welcome from '@/components/Welcome'
import Book from '@/components/Book' // これを追加

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Welcome',
      component: Welcome
    },
    {
      // これを追加
      path: '/books',
      name: 'Book',
      component: Book
    }
  ]
})

これで上記で作成したBook.vueコンポーネントに/booksでアクセスできるようになりました!
http://localhost:3000/#/booksにアクセスすれば、BookリソースをGrailsのAPIを経由して閲覧、追加することが出来ます!

なお、server側もclient側も、基本的にはコードの修正をGradleが自動的に検知してコンパイルしてくれます。
が、ファイルの追加のタイミングなどによっては自動検知が走らない事があるようなので、明らかに修正が反映されていなかったする場合は一旦アプリケーションをctrl cで停止してから再起動すれば問題ありません。

JavaScript用の外部ライブラリ(パッケージ)を追加する

さて、外部ライブラリ(パッケージ)とは、JavaScriptから扱うサードパーティー製のライブラリのことです。
通常であればnpmyarnといったパッケージ管理ツールでインストール、管理します。

個人的にこの辺りがフロントエンドを勉強する際に一番ストレスに感じる部分です。
色々ツールがあり過ぎてよく分からん!という感じですね。。。

GrailsのVue.jsプロファイルではそんな悩みから開放されます。
JavaScript専用のパッケージ管理ツールをインストールする必要はありません。
全部Grailsが面倒を見てくれます。

そのために必要な準備はとっても簡単。
単純にclient/build.gradleに以下を追加するだけです。

task addLibraries(type: YarnTask, dependsOn: 'yarn') {
  args = ['add', 'axios']
}

このGradleタスクを使うことで、このプロジェクトに上記で指定したJavaScriptパッケージ(axios)を追加することが出来ます。。
上記を保存したら、

[koji:gvue]$ ./gradlew client:addLibraries

を実行すればOKです。
すでにnpmyarnを利用したことがある方なら何となく察しは付いているかもしれませんが、上記のGradleタスクは実際のところ、

yarn add axios

と同意です。
つまり、自動的にパッケージ(今回はaxios)がダウンロードされるのはおまけで、重要なのはpackage.jsonyarn.lockの2ファイルに必要な情報が記載される事です。
その2ファイルがちゃんと更新されるので、以降このプロジェクトをcloneした場合などは、

./gradlew client:yarn_install

を実行するだけでOKです。

もしさらに別のパッケージを利用したくなったら、単純にargsリストにそのパッケージ名を追加するだけです。

task addLibraries(type: YarnTask, dependsOn: 'yarn') {
  args = ['add', 'axios', 'jsonwebtoken']
}

これで同様に

[koji:gvue]$ ./gradlew client:addLibraries

を実行すれば同じように全ての準備が自動で完了します。

詳細はgradle-node-pluginを参照してください。

追加したパッケージを利用する

追加したaxiosを利用してfetchを置き換えつつ、Bookの完全なCRUD機能を実装します。
client/src/components/Book.vueの中身を以下のように書き換えます。

<template>
  <div class="row">
    <h1>Books!</h1>
    <input type="text" value="" v-model="title" @keyup.enter="add">
    <button @click="add">Add</button>
    <ul>
      <!--本の一覧-->
      <li v-for="book in books" :key="book.id">
        <!--本のタイトル、編集ボタン、削除ボタンを表示-->
        <div :class="notEditing(book)">
          {{book.title}} (ID: {{book.id}})
          <button @click="edit(book)" class="btn btn-primary">Edit</button>
          <button @click="remove(book)" class="btn btn-danger">Del</button>
        </div>
        <!--編集フォーム-->
        <div :class="editing(book)">
          <input type="text" value="" v-model="book.title" @keyup.enter="update(book)">
          <button @click="update(book)" class="btn btn-primary">Update</button>
        </div>
      </li>
    </ul>
  </div>
</template>
<script>
import axios from 'axios'
export default {
  name: 'Book',
  data () {
    return {
      title: '',
      books: [],
      editingBooks: []
    }
  },
  methods: {
    // サーバに追加処理を投げる(CRUDのC)
    add: function () {
      axios.post(`${process.env.SERVER_URL}/book`, {title: this.title})
        .then(response => {
          this.books.push(response.data)
          this.title = ''
        })
        .catch(this.err)
    },
    edit: function (book) {
      const editingBook = this.editingBooks.find(obj => { return obj === book })
      if (editingBook === undefined) {
        this.editingBooks.push(book)
      }
    },
    // サーバに更新処理を投げる(CRUDのU)
    update: function (book) {
      axios.put(`${process.env.SERVER_URL}/book/${book.id}`, {title: book.title})
        .then(response => {
          const index = this.editingBooks.findIndex(obj => { return obj === book })
          this.editingBooks.splice(index, 1)
        })
        .catch(this.err)
    },
    // サーバに削除処理を投げる(CRUDのD)
    remove: function (book) {
      axios.delete(`${process.env.SERVER_URL}/book/${book.id}`)
        .then(response => {
          const index = this.books.findIndex(obj => { return obj === book })
          this.books.splice(index, 1)
        })
        .catch(this.err)
    },
    // 処理ではなくてCSSオブジェクトを返すメソッド
    editing: function (book) {
      const isEditing = this.editingBooks.find(obj => { return obj === book })
      return {
        editing: isEditing !== undefined,
        notEditing: isEditing === undefined
      }
    },
    // 処理ではなくてCSSオブジェクトを返すメソッド
    notEditing: function (book) {
      const editingObj = this.editing(book)
      return {
        editing: !editingObj.editing,
        notEditing: !editingObj.notEditing
      }
    },
    err: function (error) {
      alert(`HTTP Status code: ${error.response.status}.\n${error.response.data.message}`)
    }
  },
  // サーバから一覧を取得する(CRUDのR)
  created: function () {
    axios.get(`${process.env.SERVER_URL}/book`)
      .then(response => {
        this.books = response.data
      }).catch(this.error)
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  .editing {
    display: block;
  }
  .notEditing {
    display: none;
  }
  ul li {
    border-bottom: 1px dotted black;
    padding: 6px 0 6px 0;
  }
</style>

JavaScriptが苦手なので効率的でなかったりバッドプラクティスな部分が多々あると思います。。。
これで再度http://localhost:3000/#/booksにアクスすると以下のような感じでちゃんとBookのCURD機能が利用できることが確認できます。

まとめ

如何だったでしょうか。
とりあえずGrailsを使えば特に何の設定もなしにサーバサイド、そしてフロントエンド(Vue.js)アプリケーションが作成できることがお分かり頂けたのではと思います。

今回の内容は単純にGrailsのVue.jsプロファイルを利用する際の手順になります。
これらが分かれば後はそれぞれ普通にGrails、Vue.jsの使い方を調べるだけでOKだと思います。

注意点

Docker上で動かす場合

clientのVueを動かすNode.jsへのアクセスために、Dockerコンテナを起動する際にポートを開けていてもそのままではアクセスできません。(-p 3000:3000
この場合は、Dockerコンテナ上でclient/config/index.html内の、host: 'localhost'host: '0.0.0.0'にする必要が有ります。

フロントエンドが意図しないポートで起動する場合

デフォルトではフロントエンドのVue.jsアプリケーションはポートが3000で起動しますが、もしコレが3001で起動した場合、すでに別のアプリケーション、もしくは前回このVue.jsアプリケーションを停止した際に正常にアプリケーションが停止せずにポートを掴んだままになっています。
lsof -i:3000などでポート3000番を掴んでいるプロセスを確認してから killコマンドなどでそのプロセスを停止してください。

Node.jsを使っていないわけではない

yarnやnpmと言ったツールをインストールする必要は無いのがこのGrailsのVueプロファイルの素晴らしい点ですが、node.jsを利用していないわけではありません。
node.jsやnpm、yarnといったツール群はclient/.gradle/配下に自動的にダウンロードされていて、Grails(Gradle)が上手いことやってくれています。なので意識する必要は基本的にないと思いますが、この部分は覚えていて損はないと思います。

参考

Using the Vue.js Grails Profile
Vue.js
VueRouter
gradle-node-plugin