uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

React Meets Grails 〜ReactはエンタープライズSPAの夢を見るか?〜

本記事は、G*Advent Calendar 2015の24日目の記事です。 昨日は it__sseiさんの記事で、明日はtouchez_du_boisさんの記事です。

(2016/1/22追記) id:bati11_ さんが、本記事で作成しているボイラープレートコードのReact部分について、ステップバイステップの詳細かつ丁寧な以下の解説記事を執筆されております。是非併せてご参照ください。

bati11blog.hatenablog.com bati11blog.hatenablog.com bati11blog.hatenablog.com

目次

はじめに

本記事では、ReactによるSPAアプリをGrailsのフロントとして使用する方法を説明します。

以下が対象読者です。

  • Reactアプリを開発するときのGrailsの設定が知りたい人
  • サーバサイドJavaをずっとやってきて、モダンなJSの知識や経験があまりないけど、最近Reactってのが話題になっているのがさすがに気になるので挑戦したい人

背景

昨今では、ReactとかES2015を筆頭とするJSの開発技術が成熟してきており、Javaで開発するWebアプリのフロント側などは業務システムを含め近未来にSPAが席巻するだろう、と思っています。従来は高かったSPAの開発コストが、Reactベースの高機能部品群とかUIビルダみたいなものの登場と成熟により低下していき、結果SPAの利点(使い勝手の良さ、セキュリティ向上、テストしやすさなど)が損益分岐コストを上まわってくるだろうからです。

しかし、その場合でもサーバサイドはRDBMSが捨てられて、AWS LambdaとかAPIゲイトウェイとかマイクロサービスに一挙に置き換わられることは(当面)なく、ドメインモデリングに基づいたGrailsが有効だろうと思っているのです。

この仮説、すなわち一般的な業務アプリのフロントをReact/SPAで開発することの実現性や可能性について感触を探ってみよう、というのがこの記事のもう1つの目的です。

作成するサンプルアプリケーション

herokuで起動している実行例はこちらGithub上のソースコードこちらです。Webサイトではなく業務アプリっぽいものを想定したCRUDっぽいものです。

画面キャプチャも示します。

ちなみに完成度高くないのであしからず。

方針

GrailsアプリでJavaScriptライブラリを使用するのは、asset pipelineを使うのが標準的です。しかし、React使用のために検討したところ、以下の問題に直面しました。

  • JSのライブラリでasset化されていないものについては自前で配置する必要がある。要はnpmエコシステムを活用できない(この時点で致命的?)。
  • asset化されているとしても最新版とは限らない。たとえば、react-assetpipeline pluginが現時点(2015/12/24)でサポートしているのはReact 0.13.2。
  • ES6トランスパイルの際のエラー発生箇所がわかりにくい(わからない?)。
  • Reactの特長であるホットリロードに対応できない。

加えて、エラー発生時に、Web上の情報が豊富にあるかどうかも含めて考えると、asset pipelineで本格的にモダンJSの開発を行うのは今のところは難しそうです*1

なので、今回はnpmのエコシステムをそのまま使用するようにし、asset pipelineは使用しないようにします。GSPもSitemeshもいっさい使いません。GSPについては、本来ならReactを画面の一部に摘要することも可能なので理論的にはありうるのですが、ReactのルーティングライブラリであるReact Routerを使用するために断念しました*2

フォルダ構成

上記方針を踏まえ、以下のように、Grailsアプリケーションのプロジェクトフォルダ(GRAILS_PROJ)の配下に、grails-appと並列するフォルダreact-appを作成し、そこにnode moduleとしてReactアプリを配置します。

ただし、実行開始点となるHTMLファイル(index.html)およびReactアプリのコンパイル結果であるJSファイル(bundle.js)は静的ファイルとしてGRAILS_PROJ/web-app/配下に配置します。

GRAILS_PROJ
├── grails-app
│   └── domain
│        └── sample
├── scripts
│   └── _Events.groovy
├── src
│   └── templates
│       └── war
├── web-app
│   ├── index.html
│   └── js
│       ├── 89889688147bd7575d6327160d64e760.svg
│       └── bundle.js
└── react-app
     ├── node_modules
     ├── package.json
     ├── src
     │   ├── ajax.js
     │   ├── components/...
     │   └── index.js
     ├── server.js
     ├── hot.webpack.config.js
     └── webpack.config.js

JS関係の用語について

以降、いくつかの用語がでてきますので、JSに詳しくない方向けに簡単に解説しておきます。

用語 説明
Node.js JavaScriptインタプリタV8をコアとするJS処理系
JSX Reactで仮想DOMの構築に使用するJavaScriptに対する拡張記法
ホットリロード Reactの文脈では、個々のJSファイルが変更されたときに、状態(変数の値)を保持したまま、コード変更をアプリに反映し実行を継続すること。例えばモーダルダイアログを開いた状態で、JSコードのロジックを変更して保存すると、モーダルを表示したまま画面内容が変化する、といったことができる
ES2015 以前はES6と呼ばれていたEcmaScriptの標準仕様。本稿ではReactアプリの記述に全面的に使用

準備

本記事のサンプルコードを試すには、以下を事前にインストールしておくことが必要です。

  • Grails 2.5.3
  • Node.js(npmコマンドを使用)

Node.jsのインストールには、私はndenvを使いました。

Reactアプリ側で、Reactライブラリ本体やwebpackその他必要なものは、npm installコマンドで自動的にインストールされるので個別のインストールは不要です。

使用しているnpmライブラリやツールの主なものをNode.jsを含め簡単に紹介します。(参考:サンプルコードのpackage.json)

ツール 説明
webpack Node.jsでブラウザ上で動作するJSコードを開発する際に使用する、大艦主義的とも呼ばれる包括的ツール。トランスパイル、ビルドツールCSSコンパイルなどの機能を含む
npm Node package manager。Node.jsに含まれるパッケージ管理ツール
ESLint JSの静的解析ツール。ES2015に対応する
Babel 以前は6to5と呼ばれていたトランスパイラ。JSXも標準でサポート。本稿ではwebpackのローダの一つとして使用

以降、Grails側、Reactアプリ側の設定をそれぞれみていきます。

Grails

Grailsアプリ

Web APIを実装するGrailsアプリを開発します。ここでは簡単に、ドメインクラスBookを定義し、Restリソースとして公開するために@Resourceアノテーションを指定します。

GRAILS_PROJ/grails-app/domain/sample/Book.groovy

package sample

import grails.rest.Resource

@Resource(uri="/api/books")
class Book {
  String title
  Integer price
  static constraints = {}
}

context rootの変更

JSアプリの通信先として整合させるため、Webアプリとしてのcontext rootを以下のようにConfig.groovyに設定しておきます。この変更はGrails 3.x系であればデフォルトでそうなっているので不要です。

GRAILS_PROJ/grails-app/conf/Config.groovy

   :
grails.app.context = "/"

JSコンパイルの呼び出し

JSコードをコンパイルするように設定します。実際のコンパイルはreact-app配下のNodeモジュールとして実行します。LinuxMacOSに依存しないように*3、以下を含むpackages.jsonGRAILS_PROJ直下に用意し、

{
  "scripts": {
    "start": "cd react-app; npm start",
    "deploy": "cd react-app; npm run-script deploy",
    "lint": "cd react-app; npm run-script lint"
  }
}

GRAILS_PROJ/scripts/_Events.groovyで以下のように実行します。

eventCompileStart = {kind ->
           :
        def proc = "npm run-script deploy".execute()
           :

JSコンパイル自体は、npmで実行されます(後述)。 これでgrails run-appgrails compile時に最新のweb-app/js/bundle.jsが生成されます。

また、1個1個のJSファイルが変更されたときの自動コンパイルはここでは走りません。走らせても良いのですが、この方式ではホットリロードに対応できないからです。webpack経由でホットリロード実現する方法については後述します。

web.xmlの設定

React Routerのbrowser historyを使用するために、web.xmlを変更します。grails install-templateを実行し、生成されるsrc/templates/war/web.xmlに以下を追加します。

GRAILS_PROJ/src/templates/war/web.xml

    <error-page>
      <error-code>404</error-code>
      <location>/index.html</location>
    </error-page>

また、context rootがアクセスされたときにindex.htmlが表示されるように設定します。

GRAILS_PROJ/src/templates/war/web.xml

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>

文字化け対策

index.htmlに以下の設定を行います。

GRAILS_PROJ/web-app/index.html

<html>
  <head>
      :
    <meta charset="UTF-8">
  </head>

Reactアプリ側

基本的にごくごく普通のReactアプリです。

JSの明示的なコンパイル

webpackの設定で、コンパイルしたJSファイルをGRAILS_PROJ/web-app/js配下に出力するようにします。

GRAILS_PROJ/react-app/webpack.config.js

  :
  output: {
    path: path.join(__dirname, '../web-app/js'),

これはGrailsのcompile event時に呼び出されますし、明示的にnpm run-script deployを実行しても良いです。

ホットリロードの設定

Reactのホットリロード(JSファイル更新検出に伴う自動コンパイル、自動リロード)に対応する機構はGrails本体では今のところ実現できないので(WebSocketで制御する必要がある)、webpackが提供するwebpack-dev-serverを使用します。詳しくは説明しませんが、設定しているのは、GRAILS_PROJ/react-app/server.jsGRAILS_PROJ/react-app/hot.webpack.config.jsです。

この設定では、localhostの3000番ポートからindex.htmlとJSアプリ(bundle.jsファイル)をダウンロードすることになります。

webpack-dev-serverを起動するには以下のようにします。

$ cd GRAILS_PROJ/react-app
$ npm start

以降はlocalhost:3000をアクセスすることで、JSソースを変更するとブラウザ画面が自動的に変更されるようになります。

CORS対応のためのリバースプロキシ

前述のホットリロード対応のためindex.htmlとJSの閲覧ポートを3000に変更すると、Web APIの通信先であるGrailsアプリのデフォルトポート番号8080と異なってしまい、CORS(Cross-Origin Resourc Sharing)のブラウザ制約にひっかかってしまいます。

ここでうかつにAPIサーバ側のCORSヘッダ設定を緩めてしまうのはセキュリティ上問題なので、webpack-dev-serverが提供するリバースプロキシの機能を使用し、"/api"で始まるURIへのリクエストをGrailsアプリが実行されている8080ポートに転送するようにします。

GRAILS_PROJ/react-app/server.js

  proxy: {
    '/api/*': "http://localhost:8080", // Proxy to running Grails application.
  },

動作確認

まずcd react-appしてnpm install、そしてGRAILS_PROJに戻ってgrails run-appし、別窓でnpm startしてhttp://localhost:3000にアクセスします。 行を選択するとデータの表示、編集が可能です。

つみのこし

以下のテーマは本記事では扱えませんでした。

  • 認証の問題
  • Scaffold for React(ドメインクラスの入力・編集フォームなどの自動生成)
  • サーバサイドレンダリング
  • FluxやReduxに代表されるアプリケーション全体の枠組み
  • テスト

まとめ

ということで、この数ヶ月ほそぼそとReactを学習してきた結果を紹介してみました。

冒頭に示した「React/SPAで開発することの実現性や可能性」についてですが、個人の感想としては、現時点では即座に業務アプリ開発に大規模に、というのは難しい、というものです。

鍵となるのは、テーブルやフォームなどの汎用ライブラリの存在で、今でも数自体は雨後の竹の子のようにぼこぼことあるのですが、選定に時間をとられすぎるし、やりたいことができないときに絶望にハマります。機能が十分で成熟していて、今後長く使えそうなものがないと業務摘要はむずかしいでしょう。かといってプリミティブなもので構築するとコード量が増えます。

とはいえ、それらが達成されるのも時間の問題という気もします。現時点でも、ページ遷移(ルーティング)のライブラリReact Routerなど、魅力的な機能が利用でき、もともと「SPAに代表される複雑なJSコードを含むアプリケーション」については、現時点でも少人数での開発での適用はアリで、保守性・可読性が遥かに高いコードが開発できます。一人プロジェクトないしコントロールが効く数人のプロジェクトなら状況が許せば是非つかいたいです。

将来的には、React Nativeなどもあわせ、広く普及していく可能性は高いと感じています。なにより楽しいです。ReactプログラミングはJSPなどに例える向きもあるようですが、個人的にはSwingなどのプログラミングにも近いと思います。

では、ハッピークリスマス!

*1:あるいはJSライブラリの入手方法と考えればWebjarというのもありますが、たとえばこれってコマンドとしてのwebpackとかも使えるんでしょうかね…。

*2:Routerはトップレベルである必要がある。そうしないとReact Routerを使う意味が薄れる。React Routerは神ライブラリである。

*3:セミコロンだからwindowsでも結局だめかもわからんね…。