Grails/Vue.jsでTodoアプリ(Deploy as war)
これはG* Advent Calendar 2017の11日目の記事です。
この記事は、以下の5つの投稿で成り立っています。
Grails/Vue.jsでTodoアプリ(環境構築)
Grails/Vue.jsでTodoアプリ(CRUD)
Grails/Vue.jsでTodoアプリ(認証) 1/2
Grails/Vue.jsでTodoアプリ(認証) 2/2
Grails/Vue.jsでTodoアプリ(Deploy as war)
初めに
Webサーバ(今時で言うフロントエンド)とApplicationサーバ(Grails)を分けて、それぞれ別々にデプロイするのであれば特に考慮することはありません。
が、小さなWebアプリなどをVue.js&Grailsで開発するのであれば、Grailsのwarで一緒に簡単一発デプロイ出来た方が素敵です。
コレができればもうWebサーバもApplicationサーバも必要なく、javaコマンド一発でVue.jsとGrailsを駆使したWebアプリケーションを公開することが出来ます!
そのための手順をココでご紹介します。
前提条件
Grailsで作成したアプリーションのディレクトリは今後$GRAILS_PROJECT
とし、Vue.jsのアプリケーションのディレクトリはその直下なで$VUE_PROJECT
と記述します。
私の環境では、$GRAILS_PROJECT
は/home/koji/work/grails/gvtodo
で、$VUE_PROJECT
は$GRAILS_PROJECT/frontend
になっています。
また、上記の4つの記事で作成してきたアプリケーションをベースとして本文を記述しています。
Grails側の設定
さて、Vue.jsをGrailsのwarに含めるために、幾つかGrails側で設定して上げる必要が有ります。
なぜかというと、開発時はGrailsをポート8080、Vue.jsアプリをポート8081で捌いていたので問題ありませんでしたが、Vue.jsもGrailsのwarに含めるということはWebもAppも両方同じポート(今回は8080。変更可)で動くことになります。
そのため、Grailsが提供するAPI以外へのアクセスは全て、SPAであるVue.jsに流して上げる必要が有ります。
Vue.jsにアクセスを流すコントローラの作成
専用のAPI以外へのアクセスをVue.jsで作成したSPAにそのまま流してあげるために、専用のControllerを1個追加します。名前は何でも良いので、今回はIndexとしました。
grails> create-controller index | Created grails-app/controllers/gvtodo/IndexController.groovy | Created
IndexController.groovyの中身は以下のようになります。
package gvtodo class IndexController { def index() { // webappから読み取り。develop時は問題ないけど、warでのproductionモードだと見つからない。 // render file: grailsApplication.mainContext.getResource("index.html").file // render file: grailsApplication.classLoader.getResourceAsStream('index.html').text // render file: // ということで、Vue.jsのindex.htmlはsrc/main/resourcesに格納しておいてそこから読み込むようにする必要が有る。 response.contentType = 'text/html' response.outputStream << grailsApplication.classLoader.getResourceAsStream('index.html') response.outputStream.flush() } }
そして、API以外の全てのアクセスはこのIndexControllerのindexアクションに流すように、UrlMapping.groovyを変更します。
package gvtodo class UrlMappings { // これを追加 // src/main/webappディレクトリ用に、/staticへのアクセスはこのUrlMappingでは処理しない static excludes = ["/static/**"] static mappings = { "/manual/items"(resources: 'item') "/$controller/$action?/$id?(.$format)?"{ constraints { // apply constraints here } } // "/"(view:"/index") コメントアウト "/**"(controller: "index", action: "index") // これを追加 "500"(view:'/error') "404"(view:'/notFound') } }
Vue.jsのために認証の設定を修正
すでにAPI自体はSpringSecurityCore
とSpringSecurityREST
両プラグインによってログインしていなければ利用できないようになっています。
APIに該当しないURLの場合は全てVue.jsに流すようにしますので、Vue.jsへのアクセス自体に認証をかけてしまうと誰も使えなくなってしまいます。
ということで、$GRAILS_PROJECT/grails-app/conf/application.groovy
を以下のように一行だけ修正します。
修正箇所はinterceptUrlMap内の一番最後のpattern: '/**'
の部分のみです。
// Added by the Spring Security Core plugin: ...変更ないので省略... grails.plugin.springsecurity.interceptUrlMap = [ // for SpringSecurityREST [pattern: '/oauth/**', access: ['permitAll']], [pattern: '/api/**', access: ['ROLE_USER', 'ROLE_ADMIN']], [pattern: '/manual/**', access: ['ROLE_USER', 'ROLE_ADMIN']], // for /dbconsole [pattern: '/login/**', access: ['permitAll']], [pattern: '/dbconsole/**', access: ['ROLE_ADMIN']], // この1行だけ修正した。 // Vue.jsがこれに該当するので、コレ自体は誰でもアクセスOKにする。 [pattern: '/**', access: ['permitAll']] ] grails.plugin.springsecurity.filterChain.chainMap = [ ...変更ないので省略 ]
本番用DBの設定(今回の記事用)
warでGrailsを動作させるとデフォルトで本番用モードで起動します。
そうするとDB自体は事前にスキーマやデータ等を用意しておかないといけないので、ちょっと今回はその手間を省くために、productionモードでもdbCreateをcreate-dropにしておきます。
$GRAILS_PROJECT/grails-app/conf/application.yml
...省略... environments: development: dataSource: dbCreate: create-drop url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE test: dataSource: dbCreate: update url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE production: dataSource: # dbCreate: none dbCreate: create-drop # これに変更。あくまで今回の確認用なので注意! url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE ...省略...
これでGrails側の設定は完了です。
Vue.js側の設定
ビルド設定の修正
続いて、Vue.jsを本番用にビルドする際の設定を修正します。
修正するファイルは、$VUE_PROJECT/config/index.js
です。
'use strict' // Template version: 1.1.3 // see http://vuejs-templates.github.io/webpack for documentation. const path = require('path') module.exports = { build: { env: require('./prod.env'), // index: path.resolve(__dirname, '../dist/index.html'), // assetsRoot: path.resolve(__dirname, '../dist'), index: path.resolve(__dirname, '../../src/main/resources/index.html'), // 追加 assetsRoot: path.resolve(__dirname, '../../src/main/webapp'), // 追加 assetsSubDirectory: 'static', // assetsPublicPath: '/', assetsPublicPath: '/static/', // 追加 productionSourceMap: true, // Gzip off by default as many popular static hosts such as // Surge or Netlify already gzip all static assets for you. // Before setting to `true`, make sure to: // npm install --save-dev compression-webpack-plugin productionGzip: false, productionGzipExtensions: ['js', 'css'], // Run the build command with an extra argument to // View the bundle analyzer report after build finishes: // `npm run build --report` // Set to `true` or `false` to always turn it on or off bundleAnalyzerReport: process.env.npm_config_report }, dev: { env: require('./dev.env'), port: process.env.PORT || 8081, autoOpenBrowser: true, assetsSubDirectory: 'static', assetsPublicPath: '/', proxyTable: {}, // CSS Sourcemaps off by default because relative paths are "buggy" // with this option, according to the CSS-Loader README // (https://github.com/webpack/css-loader#sourcemaps) // In our experience, they generally work as expected, // just be aware of this issue when enabling this option. cssSourceMap: false } }
build:
の中のindex:
、assetsRoot:
、assetsPublicPath:
を修正しました。(既存のものをコメントアウトして新規に追加)
ビルドすると、index.html
はGrailsのresourcese
ディレクトリへ、それ以外のファイル(JavaScript、CSS、画像等)はGrailsのwebapp
ディレクトリ配下にコピーするようにしました。
本当は単純に全てwebapp
にindex.html
も含め、Vue.js系のファイルを放り込めればいいのですが、そうすると/
でアクセスしてきた時に、GrailsのControllerがindex.html
をrenderしようとしてもファイルが上手く見つからないなどの問題が発生しました。
そのため、細かく全ての問題をメモし忘れてしまったのですが、現状一番確実な方法はこれとなります。
URLの設定(必須ではない)
そして最後に、必須ではないのですが、$VUE_PROJECT/src/router/index.js
内の、new Router
の部分を以下のように修正します。
const vr = new Router({ mode: 'history', // これを追加してURLのハッシュ記号(#)を取る routes: [ ...省略... { // 本当にAPI以外の全てのアクセスがVue.jsに流れてきているか確認するために以下を追加 path: '/a/b/c', name: 'ToDoIndex2', component: ToDoIndex } ...省略...
warに固める
いよいよwarに固めてみます。
まず、yarn run build
で、必要なファイルをビルドしてGrailsのディレクトリに配置します。(この設定を上記のindex.jsで行いました)
[koji:frontend]$ yarn run build yarn run v1.3.2 $ node build/build.js Hash: a013ba3d49397af14f5f Version: webpack 3.8.1 Time: 11975ms Asset Size Chunks Chunk Names static/js/vendor.077af75171293270864c.js 136 kB 0 [emitted] vendor static/js/app.8ddd9f2f6956595dcdf6.js 15.1 kB 1 [emitted] app static/js/manifest.fffa9c3719c553c5d3cf.js 1.49 kB 2 [emitted] manifest static/css/app.0d9dd6962ae03b505f9fdae53bae9d85.css 192 bytes 1 [emitted] app static/js/vendor.077af75171293270864c.js.map 1.1 MB 0 [emitted] vendor static/js/app.8ddd9f2f6956595dcdf6.js.map 65.9 kB 1 [emitted] app static/js/manifest.fffa9c3719c553c5d3cf.js.map 14.3 kB 2 [emitted] manifest ../resources/index.html 471 bytes [emitted] Build complete. Tip: built files are meant to be served over an HTTP server. Opening index.html over file:// won't work. Done in 16.04s. [koji:frontend]$
そしてGrailsは普通にwarコマンドを実行するだけです。
grails> war :compileJava UP-TO-DATE :compileGroovy :findMainClass :assetCompile Processing File 1 of 25 - apple-touch-icon-retina.png Processing File 2 of 25 - favicon.ico ...省略... :bootRepackage :assemble BUILD SUCCESSFUL Total time: 12.179 secs | Built application to build/libs using environment: production grails>
Gradleの機能を使えば普通に1コマンドで出来るようになると思いますが、実はGradleはほぼまともに触ったことが無いので今回はスルー. これで完了です。
warを実行する
warファイルは$GRAILS_PROJECT/build/libs/gvtodo-01.war
として作成されています。
実際にこれを適当なディレクトリに持って行って実行してみます。
[koji:gvtodo]$ mv build/libs/gvtodo-0.1.war $HOME/Desktop [koji:gvtodo]$ cd $HOME/Desktop [koji:Desktop]$ java -jar gvtodo-0.1.war Configuring Spring Security Core ... ... finished configuring Spring Security Core Configuring Spring Security REST 2.0.0.M2... ... finished configuring Spring Security REST Grails application running at http://localhost:8080 in environment: production
起動しました。 これで、http://localhost:8080/や、http://localhost:8080/a/b/cにアクセスすれば、ちゃんとwarの中でVue.jsが動いていることが確認できるはずです。
どんな時にこんなとするの?
さて、今回態々Grailsのwar自体にVue.jsをまるまる放り込みましたが、そもそもフロントエンドとバックエンドはAPIを通してやり取りする粗結合なものにするのが世の流れなので、今回のようにwarの中にフロントエンドであるVue.jsまで含めてしまうのはあまり良い考えでは無いと思います。
ちょっとしたHTMLの修正もGrails(Tomcat)の再起動が必要にもなります。
ではいつ今回の内容が役立つかというと、それは恐らくGrails製アプリケーションをOSSとして公開、配布するタイミングだと思われます。
ユーザには単純にwarファイルを落としてもらって、javaコマンド一発でWebアプリケーションが起動する、という手軽さを提供することが出来ます。JenkinsやGitbucketと同じ手法です。
かく言う私も現在、この方法を用いたGrails製のOSSアプリケーションを作成中です。
本来は今年の夏頃に公開したかったのですが中々思うように時間をとれずに公開までには至っていません。
自分が仕事で使いたいツールだったので、すでに一応個人的に仕事で導入して利用しているので、来年のアドベントカレンダーの頃には公開出来れば良いな〜と思っています。