saba1024のブログ

どうでも良い思いついた事とかプログラミング関係のメモとか書いていきます。

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自体はSpringSecurityCoreSpringSecurityRESTプラグインによってログインしていなければ利用できないようになっています。
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.htmlGrailsresourceseディレクトリへ、それ以外のファイル(JavaScriptCSS、画像等)はGrailswebappディレクトリ配下にコピーするようにしました。
本当は単純に全てwebappindex.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の修正もGrailsTomcat)の再起動が必要にもなります。
ではいつ今回の内容が役立つかというと、それは恐らくGrails製アプリケーションをOSSとして公開、配布するタイミングだと思われます。
ユーザには単純にwarファイルを落としてもらって、javaコマンド一発でWebアプリケーションが起動する、という手軽さを提供することが出来ます。JenkinsやGitbucketと同じ手法です。
かく言う私も現在、この方法を用いたGrails製のOSSアプリケーションを作成中です。
本来は今年の夏頃に公開したかったのですが中々思うように時間をとれずに公開までには至っていません。
自分が仕事で使いたいツールだったので、すでに一応個人的に仕事で導入して利用しているので、来年のアドベントカレンダーの頃には公開出来れば良いな〜と思っています。