Grails&React CRUDビュー生成2017 〜Reactベースの管理画面構築ライブラリAdmin-On-Restの紹介〜
これはG*Advent callender 2017の24日の記事です。 昨日23日の記事は mshimomuさん、明日25日の記事は未定です。
完全に一年ぶりの記事です。みなさん、いかがお過しでしたでしょうか。今年Qiitaとかに書いた記事を別にまとめましたが、わたしはReactを良く書いてた一年でした。
本記事のテーマは以下の2つです。
- GrailsのWeb APIサーバのCRUDビューをいかに簡単に作るか
- RESTfulサーバ/任意のサーバに対する強力なダッシュボード・管理コンソール開発用ReactコンポーネントライブラリであるAdmin-On-Restの紹介、使い方
もくじ
- もくじ
- 去年までのあらすじと今年の方針
- Admin-On-Restとは何か
- Admin-On-Restのデモ動画
- Admin-On-Restの特記すべき点
- REST Clientによるデータソースの抽象化
- Admin-On-RestをGrailsに繋ごう
- Admin-On-Restのカスタマイズ
- デモ
- まとめ
- GraphQLはどこへ行ったの
- ポエム
去年までのあらすじと今年の方針
一昨年のG*Advent Calendarと昨年のG*Advent Calendarでは、バックエンドを「GrailsのRESTfulなWeb APIサーバ」それをCRUDするWebアプリケーションフロントエンドを「React SPA(redux,react-router,..)」という組合せで開発し、ボイラープレート(雛形)として実装しました。
さらに去年は、GrailsのドメインクラスからJSON Schemaを自動生成し、テーブルや入力フォームを生成するという工夫をしたのでした。 さて、今年はAdmin-On-RestというReactベースの素敵なライブラリがありますのでこれを使ってみます。
Admin-On-Restとは何か
Admin-On-Restは、主にRESTfulサーバをバックエンド(データ供給源)として、そのデータをテーブル形式のCRUD編集や ページネーション表示するようなWebアプリケーションを構築するためのライブラリです。用途としては管理画面を作ることを想定しているようです。RailsでいうRailsAdminみたいなものですね。
Admin-On-Restはreact-router, redux, redux-form, redux-saga, material-ui, i18n,認証などのReactでの定番の機構を用いて疎結合かつ汎用的に作られており、管理画面の構築のみならず、アプリの基本構造として使用できますし、他のアプリに部品として組込むこともできます。 過去にAngularJS用の高機能な管理画面ライブラリとして有名だったng-adminの開発チームがReactに移行して作っているようです。
Admin-On-Restのデモ動画
github.com (https://marmelab.com/admin-on-rest/Tutorial.html より引用)
admin-on-rest demo from Francois Zaninotto on Vimeo.
Admin-On-Restの特記すべき点
Admin-On-Restの特徴は、高機能さと、カスタマイズ性の両立です。react-router, redux, redux-form, redux-sagaなどを活用することによって、Reactというコンポーネント指向ライブラリの利点を如何なく発揮しています。「簡単に使いたければ簡単に」「実務的に必要があれば徹底的にカスタマイズする」ことができ、かつ保守性可読性を規模にスケールさせて維持し続けることができます。これはページ遷移ベースのサーバサイド画面生成技術では、人間の知力を前提にすると現実的には不可能だったことです。Reactが切り開く地平(の序の口)とも見ることができるでしょう。
REST Clientによるデータソースの抽象化
Admin-On-RestはもちろんGrailsに依存するものではありません。バックエンドは任意のRESTful Web APIサーバにも留まらず、 「データを供給する何か」として抽象化されて扱われます。以下はadmin-on-restのページにある説明図ですが、
(https://github.com/marmelab/admin-on-rest/blob/master/docs/index.md より引用)
上でいう「REST Client」は実はデータ集合を扱う基本操作を備えた汎用的なデータプロバイダー/コネクタであり、 コネクタには標準で添付されるもの以外に以下があります。
- Epilogue: dunghuynh/aor-epilogue-client
- Feathersjs: josx/aor-feathers-client
- Firebase: sidferreira/aor-firebase-client
- Parse Server: leperone/aor-parseserver-client
- LoopBack: kimkha/aor-loopback
- JSON API: moonlight-labs/aor-jsonapi-client
- GraphQL: marmelab/aor-simple-graphql-client (uses Apollo)
- Local JSON: marmelab/aor-json-rest-client. It doesn't even use HTTP. Use it for testing purposes.
- RDBMS
GraphQLは後述します。その他のフレームワークやサービス(やRDBMS)に対するものは、いずれも大きく言えばRESTfulなWebAPIに対するものなわけで、 なぜそれぞれにバリエーションが必要かといえば、特にページネーションを実現するための部分取得や、データ集合全体でどのページ範囲を見ているかを表現する流儀が異なるからです。
Admin-On-RestをGrailsに繋ごう
Grailsに繋ぐためには、GrailsのRest API自動生成で作られるAPIに対応したコネクタ(REST Client)を探すか設定するか作るか、あるいはGrails側で生成されるAPIの実装コードをカスタマイズして、上のいずれかと互換のある動作をするように変更する必要があります。今回は、Grails用のコネクタを作成します。
ちゃっちゃっと行きます。成果物のソースはこちら。
プロジェクト作成
Grailsのreactプロファイルを使用してプロジェクトを初期作成します。
# sdk use grails # SDKMAN使用 Using grails version 3.3.2 in this shell. # grails create-app --profile react grailsReactAdminOnRest # cd grailsReactAdminOnRest
reactプロファイルはGradleのマルチプロジェクト構成になっており、serverディレクトリ配下にgrailsアプリが、clientディレクトリ配下にcreate-react-appを使用したReactアプリが作成されます。
% tree -L 2 . ├── README.md ├── client │ ├── README.md │ ├── build.gradle │ ├── node_modules │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── src │ └── yarn.lock ├── gradle │ └── wrapper ├── gradlew ├── gradlew.bat ├── server │ ├── build │ ├── build.gradle │ ├── gradle.properties │ ├── grails-app │ ├── grails-wrapper.jar │ ├── grailsw │ └── grailsw.bat └── settings.gradle
ドメインクラス作成
BookとAuthorの2つのドメインクラスを作成します。BookはAuthorに所属するn:1の関連を持たせます。
// server/grails-app/domain/sample/Book.groovy package sample import grails.rest.* @Resource(uri='/api/book') class Book { String title Integer price static belongsTo = [author: Author] }
// server/grails-app/domain/sample/Author.groovy package sample import grails.rest.* @Resource(uri='/api/author') class Author { String name static hasMany = [books: Book] }
APIサーバの動作確認
# grails run-app
別端末から確認
% curl -X GET http://localhost:8080/author [] % curl -X POST --header 'Content-Type: application/json' http://localhost:8080/author.json -d '{"name":"uehaj"}' {"id":1,"name":"uehaj"} % curl -X POST --header 'Content-Type: application/json' http://localhost:8080/author.json -d '{"name":"yamada tarou"}' {"id":2,"name":"yamada tarou"} % curl -X GET http://localhost:8080/author [{"id":1,"books":[],"name":"uehaj"},{"id":2,"books":[],"name":"yamada tarou"}] % curl -X POST --header 'Content-Type: application/json' http://localhost:8080/book -d '{"title":"Groovy Programming", "price":1800}' {"message":"[class sample.Book]クラスのプロパティ[author]にnullは許可されません。","path":"/book/index","_links":{"self":{"href":"http://localhost:8080/book/index"}}} % curl -X POST --header 'Content-Type: application/json' http://localhost:8080/book -d '{"title":"Groovy Programming", "price":1800, "author": 1}' {"id":1,"title":"Groovy Programming","price":1800,"author":{"id":1}} % curl -X GET http://localhost:8080/book/1 {"id":1,"title":"Groovy Programming","price":1800,"author":{"id":1}}
サーバ側の端末に戻ってGrails APIサーバをいったん落しておきます。
^C
以降は、すべてクライアント側の設定やコード追加になります。
クライアントの初期化
clientディレクトリ配下にはpackage.jsonを含むNPMのプロジェクトが作成されています。(create-react-appベース)
不要なNPMモジュールをuninstall、不要なソース削除します。。
# cd ../client # npm uninstall react-bootstrap # rm -r src/App* src/css src/images
admin-on-restをNPMモジュールとしてインストールします。
# npm install -S admin-on-rest
Reactアプリとしての入り口を定義
index.jsを以下のように定義します。create-react-appが生成するindex.jsから不要なCSSのimportを削除しただけです。
// client/src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( <App />, document.getElementById('root') );
上でimportされているApp.jsは以下のように定義します。 AdminコンポーネントはAdmin-On-Restで構成するアプリのトップレベルのコンポーネントです。
// client/src/App.js import React from 'react'; import { simpleRestClient, Admin, Resource } from 'admin-on-rest'; import * as Config from './config'; export default props => ( <Admin simpleRestClient={simpleRestClient(Config.SERVER_URL)} /> );
サーバとクライアントの両起動
これで一応クライアントも起動するはずなので、サーバとクライアントの両方起動してみます。
# cd .. # ./gradlew bootRun -parallel
ブラウザのhttp://localhost:3000/が開き以下の画面が表示されると思います。 リソースが定義されていないので表示内容は空です。
Grails用REST Client の作成
simple.jsを元にして以下の細工をします。
- ページネーションの修正
- ソート順序の指定をGrailsに合せる(sort=[field,order]→sort=, order=)
- filterの処理はとりあえず今回は無視する
作成したgrallsRestClient.jsのソースはこちらにあります。
Admin-On-Restのカスタマイズ
さて上記の準備ができたならば、あとはAdmin-On-Restで画面を作っていくという話になります。(Grailsとは独立)。 基本的に、リソースを追加していくとそれがページになります。
表示リソースの追加(Author)
まず、Authorリソースを作成編集できるように修正していきます。 Authorリソースの編集のための画面コンポーネント群を定義するsrc/resources/author.jsを以下の内容で作成します。
// client/src/resources/author.js import React from 'react'; import { List, Datagrid, Edit, Create, SimpleForm, TextField, EditButton, DisabledInput, TextInput, } from 'admin-on-rest'; import ErrorBoundary from '../ErrorBoundary'; import Pagination from '../Pagination'; import AuthorIcon from 'material-ui/svg-icons/social/person'; export { AuthorIcon }; export const AuthorList = props => ( <ErrorBoundary> <List {...props} pagination={<Pagination />}> <Datagrid> <TextField source="id" /> <TextField source="name" /> <EditButton basePath="/author" /> </Datagrid> </List> </ErrorBoundary> ); const AuthorTitle = ({ record }) => { return <span>Author {record ? `"${record.title}"` : ''}</span>; }; export const AuthorEdit = props => ( <ErrorBoundary> <Edit title={<AuthorTitle />} {...props}> <SimpleForm save={() => console.log('save')}> <DisabledInput source="id" /> <TextInput source="name" /> </SimpleForm> </Edit> </ErrorBoundary> ); export const AuthorCreate = props => ( <ErrorBoundary> <Create title="Create a Author" {...props}> <SimpleForm> <TextInput source="name" /> </SimpleForm> </Create> </ErrorBoundary> );
ここでExportしているコンポーネント群(AuthorList, AuthorEdit, AuthorCreate, AuthorIcon)を使い、App.jsのAdminコンポーネントの子要素としてResourceコンポーネントを配置します。
// client/src/App.js import React from 'react'; import { simpleRestClient, Admin, Resource } from 'admin-on-rest'; import { AuthorList, AuthorEdit, AuthorCreate, AuthorIcon, } from './resources/authors'; import * as Config from './config'; export default props => ( <Admin simpleRestClient={simpleRestClient(Config.SERVER_URL)} > <Resource name="author" list={AuthorList} edit={AuthorEdit} create={AuthorCreate} icon={AuthorIcon} /> </Admin> );
これでクライアントで以下のようにAuthorを追加、編集できるようになります。 AuthorからBookへの依存がないので、Bookを作成しなくても作成できます。
表示リソースの追加(Book)
次に、Bookリソースの編集機能を追加します。 画面コンポーネント群を定義するsrc/resources/book.jsを以下の内容で作成します。
Bookは外部キーでAuthorを参照しているので、画面上での参照・編集できるように
// cliet/src/resources/book.js import React from 'react'; import { List, Datagrid, Edit, Create, SimpleForm, TextField, EditButton, DisabledInput, TextInput, NumberInput, ReferenceInput, ReferenceField, SelectInput, } from 'admin-on-rest'; import ErrorBoundary from '../ErrorBoundary'; import Pagination from '../Pagination'; import BookIcon from 'material-ui/svg-icons/action/book'; export { BookIcon }; export const BookList = props => ( <ErrorBoundary> <List {...props} pagination={<Pagination />}> <Datagrid> <TextField source="id" /> <TextField source="title" /> <TextField source="price" /> <ReferenceField label="Author" source="author.id" reference="author"> <TextField source="name" /> </ReferenceField> <EditButton basePath="/book" /> </Datagrid> </List> </ErrorBoundary> ); const BookTitle = ({ record }) => { return <span>Book {record ? `"${record.title}"` : ''}</span>; }; export const BookEdit = props => ( <ErrorBoundary> <Edit title={<BookTitle />} {...props}> <SimpleForm> <DisabledInput source="id" /> <TextInput source="title" /> <NumberInput source="price" /> <ReferenceInput label="Author" source="author.id" reference="author"> <SelectInput optionText="name" /> </ReferenceInput> </SimpleForm> </Edit> </ErrorBoundary> ); export const BookCreate = props => ( <ErrorBoundary> <Create title="Create a Book" {...props}> <SimpleForm> <TextInput source="title" /> <NumberInput source="price" /> <ReferenceInput label="Author" source="author.id" reference="author" allowEmpty> <SelectInput optionText="name" /> </ReferenceInput> </SimpleForm> </Create> </ErrorBoundary> );
外部キー参照するフィールドについて、BookListではReferenceFieldを、BookEdit, BookCreateではReferenceInputを使用します。BookCreateではReferenceInputコンポーネントにallowEmpty属性を指定する必要があります。
Grailsが出力するBookのJSONレコードには、authorに対する外部キーが、
{"id":1,"title":"Groovy Programming","price":1800,"author":{"id":2}}
のように含まれるため、ReferenceField、ReferenceInputのsource属性に"author.id"を指定しています。
Bookリソースを増やしたのでApp.jsのAdmin配下にResourceコンポーネントを追加します。
// client/src/App.js : export default props => ( <Admin restClient={restClient(Config.SERVER_URL)}> <Resource name="book" list={BookList} edit={BookEdit} create={BookCreate} icon={BookIcon} /> <Resource name="author" list={AuthorList} edit={AuthorEdit} create={AuthorCreate} icon={AuthorIcon} /> </Admin> );
これによって生成される新規作成や編集画面は以下のようになります。BookのauthorフィールドはAuthorへの外部キーなので、AuthorレコードのnameからAPIを通じて引っぱってきて選択肢として表示されています。
その他
React 16を使用するので、異常時の結果を判りやすくするためのコンポーネントErrorBoundary.jsをこちらを参考にして使用します。
また、デフォルトのAdmin-on-Restのページネーションでは全ページへのリンクを表示できるがその機能を除去したバージョンのページネーションUI Pagination.jsを作成します。
デモ
オンラインデモ準備は間に合いませんでした。できたらリンクはります。 ソースコードはこちらにあります。
まとめ
以上のように、ページベースアプリではなく、Web API化することで、クライアントはGrailsとは完全に独立したものを使用できるようになるわけです。Admin-On-Restは発展甚しく、機能も汎用性も高いので、是非使っていきたいところです。
もっとも、去年はJSON Schemaでドメインクラスからメタ情報を抽出し、表の項目やフォーム画面なのカスタマイズ、バリデーション処理なども生成することを試みました。今年はそこが抜けていて、フィールドの順序やフォーム中のデータ項目を明示的に指定していく必要はあります。
GraphQLはどこへ行ったの
GraphQLはRESTfulのカウンターとして出てきた技術で、高機能高効率なクエリを可能にすると同時にAPIを強く型付けするものです。
GrailsにはGORM GraphQLプラグインがあり、ドメインクラスを元にして、QraphQLスキーマの生成や、grapql browserの組込み、さらにはGraphQLクエリに対するハンドラの実装を行うことができ、一般的なサーバサイドのGraphQLソリューションが実現されています。
さらに、前述のようにAdmin-On-Restはapollo-clientをベースとしたGraphQLコネクタ(aka RestClient)を利用可能です。
ですので、当初のアドカレの予告タイトルではGraphQLでやったるで! というのを狙っていたのですが、調べていく内に(時間がなくなった!というのもありますが)、今いまでは、管理画面程度のデータの列挙や編集のために、GraphQLを適用するのはかならずしもベストではないな、と思っています。
その理由は、なんだかんだいってRESTful APIは、ページネーションとかは微妙に違がっていても、さまざまなサーバをまたがって共通的なモデルではあるわけです。カスタマイズもたかがしれてます。
GraphQLは、高機能でありもちろんページネーション的なものを実現可能ですが、いくつかの流儀があって、つまりRelayなのかGraphcoolなのか独自なのか、といった屋上屋的な規約が必要です。GrailsのGORM GraphQLプラグインは素のGraphQLしかないので、たとえばGraphchool規約に従うようなGraphQLマッピングを追加する、もしくは逆にGraphQLコネクタをaor-simple-graphql-clientなどを元にして修正していく必要があります。(ここらへん、GraphQLに詳しくないので嘘言ってるかも! 自信なし)
でもそういう接続を自分でやったとしてもその選択がメジャーになるのかはわからないし、今後Grails側かクライアント側かで発展して、GraphQLがアウトオブボックスで簡単に繋がるプラグインかコネクタが今後できてくるんではないかと踏んでいます。
そういうわけで、現時点でGrailsで管理画面を実装する程度では、RESTfulを採用した方がリーズナブルで現実的であり、特に他の理由でGraphQLサーバを仕立てたいのでない限りは、RESTfulで良いと考えています。
なお、GORM GraphQLプラグインはまだ使い込んではいませんが、GraphQLのカスタムオペレータやフェッチャーを定義する形態として、データと表裏一体に定義できる、GrailsのGORM上のDSLというのは筋が良いと思っています。
ポエム
過去にフレームワークと呼ばれたものは、構成要素に分解され、疎結合化されていく時代です。分解された要素はマイクロサービスで実現されるかもしれないし、クラウドプロバイダーが提供する機能や、FaaSで代替されるのかもしれません。要するに、フレームワークが分解し、「クラウドプラットフォーム全体が大きなフレームワークになった」のです。
そのような現代および未来のプログラミングの主眼はAPIの接合にあり、UI部はSPAがまかないます。この記事シリーズは、そういう流れでフレームワークやクラウドを捉えています。
Reactがなぜ重要か
この観点からは、まずUI部に関して、今回のAdmin-On-Restを含むさまざまなイノベーションがReact界隈から連続的に生まれてきています。Reactは今後も圧倒的に注目すべき技術であると思います。個人的にはMaterial-UI-Next、React Nativeが注目です。
GrailsとはGORMである
そしてGrailsをこの「クラウド=分解されたフレームワーク」論から見ると、Grailsの価値はデータ表現のDSL、ORMが残ると言えます。GORMを書きやすく扱いやすくするシステムがGrailsなのです。GORM GraphQLプラグインはその応用例の一つです。
みなさま、ではメリークリスマス and 良いお年を!
JWTトークン認証つきのWeb APIを作るのはGrails+Spring Security REST Pluginを使えば非常に簡単である件
先日のJGGUG WSでのLT資料を公開します。「JWT」は、ついジェーダブリュティーと読んでしまいましたが、正しい発音は「jot(ジョット)」だそうです。
ちなみに、上記デモで使用する認証スキームBearerというのがあるんですが、この由来は「持参人払い小切手(Bearer Check)」から来てるそうです*1。「持参人払い小切手」とは、自筆の署名(サイン)がしてある小切手を銀行窓口に持ってきたならば、その持ってきた人が誰であるかにかかわらず、現金を支払う、というものだとのことです。たしかにBearer認証スキームと同様ですね。
*1:帰りに杉本さんに教えていただきました。ありがとうございました
Grails 3ってどうよ+ LT大会 - 日本Grails/Groovyユーザーグループ
連休明け5/13に、JGGUG(日本Grails/Groovyユーザ会)より、上記イベントがございます。
Sring BootをGSPとscaffold、GORMなどでさらに使いやすくしたフルスタックフレームワーク、Grails3について、実務適用している人たちとの情報提供、議論を行います。また恒例のLT大会も行います。
ふるってご参加ご検討ください。
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モジュールとして実行します。LinuxやMacOSに依存しないように*3、以下を含むpackages.jsonをGRAILS_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-app
やgrails 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.js
とGRAILS_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にアクセスします。
行を選択するとデータの表示、編集が可能です。
つみのこし
以下のテーマは本記事では扱えませんでした。
まとめ
ということで、この数ヶ月ほそぼそとReactを学習してきた結果を紹介してみました。
冒頭に示した「React/SPAで開発することの実現性や可能性」についてですが、個人の感想としては、現時点では即座に業務アプリ開発に大規模に、というのは難しい、というものです。
鍵となるのは、テーブルやフォームなどの汎用ライブラリの存在で、今でも数自体は雨後の竹の子のようにぼこぼことあるのですが、選定に時間をとられすぎるし、やりたいことができないときに絶望にハマります。機能が十分で成熟していて、今後長く使えそうなものがないと業務摘要はむずかしいでしょう。かといってプリミティブなもので構築するとコード量が増えます。
とはいえ、それらが達成されるのも時間の問題という気もします。現時点でも、ページ遷移(ルーティング)のライブラリReact Routerなど、魅力的な機能が利用でき、もともと「SPAに代表される複雑なJSコードを含むアプリケーション」については、現時点でも少人数での開発での適用はアリで、保守性・可読性が遥かに高いコードが開発できます。一人プロジェクトないしコントロールが効く数人のプロジェクトなら状況が許せば是非つかいたいです。
将来的には、React Nativeなどもあわせ、広く普及していく可能性は高いと感じています。なにより楽しいです。ReactプログラミングはJSPなどに例える向きもあるようですが、個人的にはSwingなどのプログラミングにも近いと思います。
では、ハッピークリスマス!
今こそッ、始めようGrailsブートキャンプ!!!!
以下のイベントが予定されています。 Grails3対応のGrailsブートキャンプです。
Grails3というのが出たタイミングで、ちょっと取り組み直しみよう、という向きに最適です。
山本さん(id:yamkazu)NTTSOFTが講師です。
ご興味があればぜひご検討を。
G*なWeb API
Grails3もカウントダウンな感じのおり、イベント紹介です。以下引用。
G*ワークショップZは日本Grails/Groovyユーザーグループの定例イベントです。Java仮想マシン上で動作するGroovy、Grails、Gradle、Spock、vert.xといったG*技術をテーマに、ハンズオンやコードリーディングなど参加型の内容を金曜日に開催しています。
今回のG*ワークショップ"Z" 第19弾は、「G*なWeb API」と題して、昨今話題のWeb APIに関連した以下の内容です。いずれもハンズオン形式ではありません。
Grails 3でWeb APIを簡単に作ろう!
Grails 3の新機能の紹介を交えながら、GrailsでのWeb API構築がいかに容易にできるかをお話します。GrailsのREST機能と、Web Microプロファイルなどを含めてご紹介する予定です。
REAL Objects : 動的なエンタプライズAPIアーキテクチャをどう構築するか
REAL Objectsは、(例えばSmalltalkのような) メッセージ・ベースのオブジェクト指向アーキテクチャを現代の並列/分散環境で実現するものです。 現実の業務システムのような、大規模で、ドメイン・モデルが動的/多様で, 非パブリックなAPIを持つアーキテクチャをどう実現するかについて、 G*技術を (多少) 絡めつつ, お話しします (ワークショップ形式ではありません)。
講演者紹介
- 山本 和樹( @yamkazu )さん: Grails 3でWeb APIを簡単に作ろう!
- 山田 正樹( @yamadamasaki )さん: 「REAL Objects : 動的なエンタプライズAPIアーキテクチャをどう構築するか」
こちらからお申し込みください。
Grails/Groovyのサイトを構築している、名もなき静的サイトジェネレータ
Grails/Groovyのサイトは、静的サイトジェネレータで作成されています。Groovyサイトは去年ぐらいからそうだったのですが、最近Grailsもそうなりました*1。
しかしこの静的サイトジェネレータの名前がわかりません。ソースコード上は、単に「generator/SiteGenerator.groovy」で、独立したgithub projectもありません。groovy-websiteやgrails-static-websiteというプロジェクトの一部としてそれぞれでカスタム化されて機能しているというだけです。
SiteGenerator.groovyは139行ぐらいでやってることもシンプルで自明なので、独立にするまでもない、ということでしょうか。処理の実行はgradleタスクでguild.graldeに書いてあります。
gradle genrateSite
こんな感じでsite/build/site配下にHTMLなどを含むサイト全体の情報が生成されます。gradle webzip
でリンク切れチェックなどが実行された上で、サイト内容をzipで固めたものが生成されます。デプロイ機能とかはないみたいです*2。
この名もなき「サイトジェネレータ」がたいへん気に入ったので紹介します。
Markup Template Engineでページを作成する
Markup Template Engineは、Groovy 2.3で導入された、HTMLを書くためのDSLであり、HTML生成を実行するGroovy標準クラスライブラリです。ドキュメントはこちら。APIリファレンスはこちら。
たとえばGrailsサイトの、pages配下にあるトップページindex.groovyは以下のような感じで記述されます。
import model.Event layout 'layouts/main.groovy', true, pageTitle: 'The Grails Framework', mainContent: contents { div(id: 'band', class: 'band') { } div(id: 'content') { include unescaped: 'html/index.html' int eventsPerRow = 3 def allEventNames = allEvents.keySet() as List int rows=allEventNames.size()/eventsPerRow + (allEventNames.size()%eventsPerRow != 0 ? 1 : 0) for(int rownum = 1; rownum <= rows; rownum++) { int offset = (rownum-1) * eventsPerRow def cssClasses = "row colset-3-article" if(rownum==1) { cssClasses += " first-event-row" } if(rownum==rows) { cssClasses += " last-event-row" } section(class: cssClasses) { if(rownum == 1) { h1 { strong "Groovy and Grails events you shouldn't miss!" } } allEventNames[offset..(Math.min(offset + (eventsPerRow-1), allEventNames.size()-1))].each { String eventName -> Event event = allEvents[eventName] article { div(class: 'content') { // Note that the event image should be 257x180 to look nice div(class: 'event-img', style: "background-image: url(${event.logo})") {} h1 { a(href: event.url) { strong eventName br() em event.location } } time event.date yieldUnescaped event.description } } } } } } include unescaped: 'html/they-use-groovy.html' }
要するに、HTMLをプログラムとして書くわけです。抽象化、モジュール化、共通部分の括りだし、合成、型チェックなどなど、プログラミング言語が持つフル機能を享受してサイトというシステムの提供機能を記述することができます。
Markup Template Engineについてはこちらの資料もどうぞ。
大規模なサイトは、構造をもったソフトウェアなのであり、「プログラムのように」ビルドやチェックができるのが利点です。さらに静的サイトジェネレータ一般の利点として、ソースコード管理システムで管理できるのがうれしいです。データをDBに保存するCMSとかは個人的には捨てるしかないです。バックアップとか面倒すぎるし、履歴を辿れないのもアレだし。かといってGititとか使ってみたこともありますが、結局私には耐えられなかったとです。
生のHTMLを使える
先のindex.groovyでは、include unescaped "html/index.html"
のようにHTML断片を使えます。
これは実際問題としては現実的です。Excelをたとえば XLS2HTMLTABLEアドインなどでテーブルに変換したりしたものをページの一部に貼る、みたいな状況ではHTMLを「そのまま」使いたいときがあり、必須です。半端なwikiとか、「不完全なHTML」だけしかつかえないCMSなどは、動的だろうが静的だろうが捨てるしかないと私は思うわけです。
「 データ」をモデルクラスとして定義、使用する
わたしはこれが一番面白いと思ったのですが、サイトで使用するデータを「モデル」として定義します。たとえば以下が「モデル」です。
- メニューの選択項目(リンクであったり、サイト内のページであったり)
- 「書籍」一覧を含むのであれば、書籍クラス(Book.groovy)
- サイトで配布物jarを列挙しているのであれば、配布物のクラス(Distribution.groovy)
さらにこれらのモデルは、sitemap.groovyというDSLで、その値を簡潔に定義できます。 sitemap.groovyの例はこちら。
Webサイトが大規模でも大規模じゃなくても、静的であっても、それがデータを扱っていることは間違いありません。静的データをHTMLのULやliでベタに書くということは、もしやたいへんに愚かしいことではなかったのか、と考え込まずにはいられません。JSで書くのもアレだし。
こちらのサイトによれば、sitemap.groovyとモデルクラスで設定するモデル群は、「低コストなデータベース」だそうです。 まさしく。コンパイル時に使用したい、データベースか。うーん、この発想はなかったな。
もちろん、既存の静的ジェネレータでも「ブログエントリ」とかはデータなんですが、もっと汎用的なもの。
リンク切れチェック
checkDeadLinks
タスクでサイト全体のリンク切れのチェックが実行されます。
事前インストールフリー
Gradleだから当たり前なんですが、サイトのプロジェクトソースさえ入手すれば、Java以外のインストールなしに、GroovyやGradle、gvmやらの明示的なインストールなしで、即刻ページ生成が可能となります。gradle wrapperってやつです。
その他
font-awesomeとかtwitter-bootstrapとかをデフォルトでだいたい含んでます。
まとめ
Spud-CMSのGrailsプラグインとかも遍歴しましたが、社内のサイトとかは、これからは当面これで作っていこうと思います。apacheだけでいいです。tomcatとかもういいです。