Grailsでお手軽にVue.jsアプリケーションを作成する
概要
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-cli
のwebpack
プロファイルで作成されたものと同様のものになっています。
起動
プロジェクトのトップディレクトリに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.groovy
とAuthor.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から扱うサードパーティー製のライブラリのことです。
通常であればnpm
やyarn
といったパッケージ管理ツールでインストール、管理します。
個人的にこの辺りがフロントエンドを勉強する際に一番ストレスに感じる部分です。 色々ツールがあり過ぎてよく分からん!という感じですね。。。
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です。
すでにnpm
やyarn
を利用したことがある方なら何となく察しは付いているかもしれませんが、上記のGradleタスクは実際のところ、
yarn add axios
と同意です。
つまり、自動的にパッケージ(今回はaxios)がダウンロードされるのはおまけで、重要なのはpackage.json
とyarn.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
公開日:2018/04/25