GrailsでWebアプリケーションを開発運用しています。現在一部フロントエンドから順次Nuxt.js上に移行している最中です。
ここまで来るのに自分がどんな問題で悩んだのか、などなどを備忘録として以下に纏めます。
上から順番に時系列になっています。

すべてサーバサイド

所謂Webアプリケーションです。
すべてのViewはGrailsのGSPファイルに記述されていて何の問題もなくスマートに動作していました。
ある時、かなりフォームの多い、ユーザの入力を元にデータをサーバから取得して、その結果を元にして様々な条件でDOMを書き換える、という機能を実装しました。
リリースした時点では完璧に思えていましたが、やはりDOMを書き換える部分をすべてjQueryで実装していたため早晩「あ、もう無理」という状態に陥りました。
この時点でコレでは不味い、モダンなフロントエンド開発環境について勉強しなければ、と感じるようになりました。
ただし、JavaScriptを書くのにwebpackとかbabelとか色々ツールとか入れたくないという思いが強くありました。
そこで1ファイルだけ読みこめば使えるというVue.jsに手を出すようにしました。

以下、長々書きますので、お時間のない方のために結論を申しますと、中途半端にVue.jsを追加していくよりは、Nuxt.jsを使って必要な機能を少しずつ分割していくのが最終的に楽だと思います。

viewに直接Vue.jsを埋め込む

まず最初の段階です。Vue.jsファイルをダウンロードしてきて、必要なGSPファイルで読み込むだけです。
Vue.js自体の勉強で色々時間がかかったりはしましたが、全て順調でした。
なるほどコレがリアクティブなフロントエンドかと。jQueryで自前でリッスンしてDOMを書き換えていたのが嘘のように楽チン!
ただやはり進めていくうちに嫌な匂いを感じるようになりました。
それが顕著だったのがテンプレート(HTML)です。

Vue.jsがテンプレート(HTML)を表示するなら、

Vue.component('my-component', {
    template: "<div>何かのsomething</div>"
}

という感じで、Vueコンポーネントのオブジェクトの中にただの文字列として指定することになります。
コレが一行なら問題ないでしょうが、当然各コンポーネントのHTMLが1行に収まり切るわけがありません。
ES6のテンプレートリテラルを使えば少しだけマシになりますが、それでもあくまで文字列です。
IntelliJなどの統合開発環境で良い感じにHTMLを編集することが出来ません。
この時点で、もうこの方法だと無理があるな、ということに気づいてきました。

Vue CLIでVue.js専用プロジェクトを作る

もうフロントエンド界隈の便利ツールを使わない開発は無理なんだな、と悟りました。
ココでようやく自分も専用ツールを使ってフロントエンド開発に入門する決心をしました。
「でもwebpackとかbabelとか色々ツールあって大変そう。。。」と思っていましたが、Vue.jsの場合はVue CLIという、これを使えば必要なものが全部一発で揃えられるよ!という、私のようなド素人に非常に親切なツールがあったので即採用。

Vue CLIでVue.jsアプリケーションを開発すると.vueファイルが使えるようになります。
なんとこのファイルはHTML(テンプレート)、JavaScript、CSSを一つに纏めて書くことが出来る優れものです!
ちゃんとコンポーネントを分けないと結局肥大化するのは目に見えていますが、これを使えばHTML(テンプレート)を管理するのが大変、という問題を解決できる上に、CSSも各々のコンポーネントで必要なものだけをその場で管理できるようになります!まさに理想郷です!

余談ですが、Webフレームワークを利用しない時代のPHPで、HTMLとPHPが1ファイルに記述されていた時代に戻ってきたような気がします。
現在は諸々上手いことCSSフレームワークなどを使ってコンポーネント化する土台があるので状況は異なりますが。

さて、最終的にVue.jsアプリケーションをビルドして普通のJavaScript、CSS、そして一つのindex.html(エントリポイント)を生成して、コレをブラウザに返す必要が有ります。これらはyarn buildで全て一発で生成できるので難しいことはありません。

すでにGraisとは関係ないプロジェクト(ディレクトリ)で開発しているので、この成果物をNginxのドキュメントルートなど設置すればもう完了なのですが、そこに思考がまだ及んでいないド素人です。
Grailsのweb-appディレクトリに配置して、GrailsからそのVue.jsアプリ(index.html等生成された成果物)をクライアントに返してもらう方式にしました。

Vue.jsのビルド時に生成される成果物の吐き出し先を以下のような感じでGrailsのweb-appディレクトリに変更しました。

config/index.js

 ...
  build: {
    // index: path.resolve(__dirname, '../dist/index.html'),
    index: path.resolve(__dirname, '../../web-app/hoge/index.html'),

    // Paths
    // assetsRoot: path.resolve(__dirname, '../dist'),
    // assetsSubDirectory: 'static',
    // assetsPublicPath: '/',
    assetsRoot: path.resolve(__dirname, '../../web-app/hoge'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/hoge/',
 }
...

Grails側では、UrlMapping.groovyに以下の設定を追加しました。

 static excludes = ['/images/*', '/hoge/*']

こうすることで、Vue.jsアプリケーションはyarn buildで成果物をGrailsのweb-app配下に出力してくれて、/hoge/へのアクセスが来た場合に、GrailsはControllerで処理するのではなく、web-appディレクトリをドキュメントルートとするような形で普通に静的なファイルをブラウザに返してくれるようになりました。

これでやっと以下の当初の問題点を解決することが出来ました。

  1. jQueryでDOMをイジるのが辛い
  2. Vue.jsだけだとテンプレートを管理するのが辛い

そして結果的に、フロントエンドとして完璧にバックエンド(Grails)と分離することが出来ました。
ただし、新たに以下の問題点が発生しています。

  1. 毎回yarn buildしてGrailsに成果物を渡すのが面倒
  2. 開発時はVue.jsを分離できたけど、本番実行時には結局Grailsと分離できていない(ルーティングがGrails任せ)
  3. Vue.jsでページが増えると、VueRouterの内容が肥大化する

1番は自動化することである程度は回避可能ですが、結局自分でcommit&pushする必要が有ります。
2番はNginxのドキュメントルート配下に成果物をデプロイすることで回避できそうです。
3番、コレが一番嫌な問題でした。ページを追加する毎に毎回VueRouterに手動でURLを指定してあげる必要が有ります。
SEO的に、一般ユーザが触れる部分でVueRouterを弄るのは致し方ないとしても、管理画面系でこれは辛いです。

そしてNuxt.jsへ

ということで、現在は上記のVue CLIで作成したVue.jsアプリケーションをNuxt.js上に移しました。
Nuxt.js自体はVue.jsの為のフレームワークですので、既存のVue.jsアプリをNuxt.js上に移行すること自体はそこまで大変ではありませんでした。
Nuxt.jsであれば、pagesディレクトリに.vueファイルを格納すれば、pages配下のディレクトリ名、ファイル名がそれぞれ自動的にURLとなり、VueRouterの設定を自動で生成してくれます!

また、サーバサイドレンタリングを利用すれば、ユーザがアクセスしてきたらまずはindex.htmlが返されて、ブラウザから更にAPIを叩いて必要なデータを取得&表示する、というような流れをサーバ側で行った後に、その結果をブラウザに返すことが出来るようになります。
これで、DOMがまだ生成されていない状態でブラウザに一瞬表示されて、その後ボコボコDOMが生成されてなんか表示がガタガタする、という問題を簡単に回避することが出来ます。

サーバサイドレンタリングはSEOに良い(GoogleボットがHTMLをちゃんと解析できる)というメリットがあるかとも思っていましたが、最近のGoogleボットはページを読み込んだ後に自動的に動くJavaScriptが生成するDOMぐらいは普通に解析してくれているようなので、この部分は特に気にする必要はないと思います。

Nuxt.js自身ををサーバとして利用するようにしましたので、(yarn build && yarn start)、あとはNginxから特定のURLの場合には、このNuxt.jsにプロキシするようにしました。

Nginxの設定は以下のような感じです。

    location /hoge {
        try_files $uri @nuxt_frontend;
    }

    # Settings for assets directory of Nuxt.js
    location /_nuxt_frontend {
        # This url is used for assets directory.
        # Threfore it must be proxy to Nuxt.js. NOT TO Application server!!
        try_files $uri @nuxt_frontend;
    }

    # Nuxt instance for frontend
    location @nuxt_frontend {
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:3000;
        proxy_cookie_path / /;
    }

これで、ルーティングもGrailsから完璧に切り離すことができました。
現在、このような流れで少しずつviewをNuxt.js側に移動させていっています。

今ある問題点

特定のURLの場合のみ、NginxがNuxt.jsにプロキシするようにしたので、結果的にルーティングの依存先がGrailsからNginxに切り替わっただけ感は否めません。
ただし、基本的にすべてのフロントエンド機能がNuxt.jsで実現できたタイミングで、クライアントからのアクセスを全てNuxt.jsに流すようにすればよくなる予定です。いずれ自然と解決するものなので、問題というよりはタスク、という感じでしょうか。
既存のGSP(サーバサイド生成する古き良きview)と、Nuxt.jsで追加していくページで、それぞれCSSが共有しづらい、という点も有りますが、これも何れNuxt.jsに引っ越しが完了すれば解決ですね。

サーバサイドのviewのみでVue.jsを使いたいとお思いの方へ

各アプリケーションの構成や要件、人員、時間などなど色々事情があるかと思います。
その点からも、既存アプリケーションにVue.jsを組み込んでしまうよりも、思い切ってNuxt.jsを導入して、少しずつ機能をNuxt.jsに移していくのが一番安全で必要な労力も少ないかな、と思います。

というのも、基本的な流れが

  1. 新機能 or 既存で軽い機能をNuxt.jsでまず作る(フロントエンド)
  2. 必要なデータをサーバから返すRESTful APIを作る(バックエンド)
  3. Nginxで特定のURLの場合のみNuxt.jsにプロキシする(本番環境)

の繰り返しになると思います。
どのような単位で切り抜くか、そしてURL設計をどうするかという部分が各アプリケーショなどによって大分異なってくるとは思いますが、原則「新しい機能を追加する」というコンテキストで作業しますので、既存のものを壊す心配がなく、精神衛生上非常によろしいと思います。

まとめ

別にどうということはない、自分の今までやって来たことの履歴です。
Vue.jsは日本語ドキュメントが本当に豊富なので非常に学習コストが低いのが素敵ですね。
さらに、ドキュメントを読んでいても「このような場合はコレコレこうすべき。それは~」みたいなスタンスではなくて、「フォームを弄る時をこうします。」という余計な押し付けがましさが無いところも自分は大好きです(あくまで個人の感想)

フロントエンド周りは本当に苦手なので、誤っていたり悪手なアプローチなどあるかもしれませんが、あくまで私のやって来たことのメモということで。