はじめに
React.jsを使って業務アプリをSPAで作ってみたので、知見というか体験談を共有したいと思う。なお、業務アプリという分野におけるReact.jsの利用に関しては、React.js Advent Calendar 2015に「業務系アプリの実装だからこそReact使おうぜ」 という記事もあるのでそちらも合わせて読んでおこう。
アプリケーションの特徴
業務アプリといっても色々あると思うが、今回のアプリケーションでは以下のような特徴があった。
- Formによる入力項目が多い&バリデーション
- 画面遷移がかっちり(遷移途中の画面にいきなりアクセスや戻ることは不可)
- アクセスコントロールがある
- CRUD操作が多い
- 表形式でデータを表示
- IEをサポート
以下、それぞれについて詳細を書いていく。
Formによる入力項目が多い&バリデーション
業務アプリといえばFormの入力項目の多い、というイメージがありますがまさにそんな感じのアプリでした。ReactでFormを扱う方法は本家のドキュメント- Formsで書かれているが、この通りにベタに書いていくとコード量が肥大化しとても辛くなる。アプリケーションの画面要件を整理するとFormで利用するコントロールやレイアウトはいくつかのパターンに絞ることできたので、汎用的なFormを生成するコンポーネントをあらかじめ部品として作ることで、Formをベタ書きすることは基本的に避けた。また、Formのバリデーション(クライアントサイド・サーバサイド)もこの汎用的なFormコンポーネントの中である程度自動的に行うようにしている。作成したコンポーネントの利用イメージとしては以下のような感じ。雰囲気はわかると思う。
render() {
return (
<div>
<DynaForm value={this.state.data} schema={this.props.formSchema} onChange={this.handleFormChange} />
<button onClick={this.register} disabled={!this.state.formState.isValid}>登録</button>
</div>
);
}
props
のschema
で渡しているオブジェクトの中にFormのスキーマ情報として利用するコントロールの種類・バリデーション情報などが設定されている。React.jsのベストプラクティスに沿って、FormのStateはFormコンポーネントでは管理せずにStatelessなコンポーネントとしている。StateはFormコンポーネントを利用する親コンポーネントにて基本的には管理。このFormコンポーネントを使うことで1画面の記述はかなりスッキリ(というかスッカスカに)した。
なお、今ならForm用のReactライブラリが色々あるので要件とマッチするのであればこれらを使ってみて良いかも。
FluxにReduxを使っている場合は、ReduxベースのFormライブラリもある。
画面遷移がかっちり
例えばForm入力の画面でも、
- 入力画面
- 入力確認画面
- 結果画面
みたいな画面のステップを踏むような遷移がある。このとき、2、3の画面にいきなりアクセスしたり、ブラウザの戻る操作で3から2に戻るようなことは防ぎたい。こういった主にForm入力を伴う遷移の画面があちこちにある。
今回、ルーティングにはreact-routerを使っていたが、こういった画面遷移を制御するのはちょっと厳しい。そもそもBookmarkableなURLである必要性は全くない(というかそれを防ぎたい)ので、1つのビューコンポーネントで画面遷移を制御することにした。ただし、似たような遷移制御はあちこちにあるのでこれも汎用的な部品として、Wizarコンポーネントみたいなのを作ることで対応した。かなり端折ってはいますがイメージとしては以下のような感じ。
render() {
return (
<div>
<Wizard handleTransition={this.handleTransition}>
<Step name="form" title="入力">
<DynaForm value={this.state.data} schema={this.props.formSchema} onChange={this.handleFormChange} />
</Step>
<Step name="preview" title="プレビュー">
<DynaForm value={this.state.data} schema={this.props.previewFormSchema} readonly={true} />
</Step>
<Step name="result" title="結果">
<DynaForm value={this.state.data} schema={this.props.resultFormSchema} readonly={true} />
</Step>
</Wizard>
</div>
);
}
アクセスコントロールがある
基本的にユーザはログインしないと画面にはアクセスできない(ログインページ、エラーページ除く)。また、ログイン後の画面でも様々な箇所でログインユーザの権限によるアクセスコントロールもある。アプリケーション全体としては大きく2つあり、
- クライアントサイドでの制御
- 権限に応じたナビゲーション表示制御
- 権限に応じた細かい表示制御
- 権限に応じたルーティング制御
- サーバサイドでの制御
- オペレーションの認可制御
みたいな感じだが、Reactが関係するのはクライアントサイドのみ。
権限に応じたナビゲーション表示制御
ナビゲーションはアプリケーションのデザイン的には全画面で常に表示するようなものであったので、各画面共通の親コンポーネントでまとめて定義しておいた。また、ナビゲーション自体をコンポーネントとして別途作成し、ナビゲーションの定義をJSONで書けるようにしておいた。そこで表示を許可するロールと一緒に設定することで、ナビゲーションコンポーネントで自動的にリンクを表示・非表示させるようにしておいた。
{
"home": {
"role": ["user"],
"url": "/home"
},
"adminPage": {
"role": ["admin"],
"url": "/adminPage"
}
}
render() {
return (
<div>
<Navigation menu={this.props.menu} />
<div className='view'>
{this.props.children}
</div>
<div className="footer">…</div>
</div>
);
}
権限に応じた細かい表示制御
ナビゲーション以外での細かい表示制御に関しては、必要な箇所で個別にログインユーザのロールをチェックして表示を切り替えていた。このために、ログインユーザの認証情報は別途親コンポーネントから自動的に渡されるようにしておき、各画面を作る際にprops
経由で常に利用できるようにしておいた。表示切替用のコンポーネントを別途作ることもできますが、JSXではJavaScriptをそのまま書けるのでシンプルに以下のように書く方法でも良いと思っています。
render() {
return (
<div>
…
{ this.props.authContext.hasRole('admin') &&
<div>You are admin!</div>
}
…
</div>
);
}
権限に応じたルーティング制御
前述のナビゲーションでリンクの表示制御はしているが、それだけだと直接URLを入力された場合はアクセスできてしまうので、画面(URL)ごとに権限に応じたルーティング制御を別途行っている(そもそもナビゲーションからは直接辿りつけない画面もある)。アクセス制御はreact-routerのonEnterで制御できるのでそれを利用した(Qiitaに記事: React Routerで認証を制御する方法 があります)。
また、そもそもreact-routerのJSXでアプリケーション全体のルーティングを定義するのはイマイチだと感じているので、JSONで別途定義できるようにラップしている。そこでロールベースでアクセス許可を定義できるようにしておいた。こうしておけば将来的にreact-routerから他のものにも乗り換えしやすい。
{
"home": {
view: "HomeView",
role: ["user"],
url: "/home"
},
"adminPage": {
view: "AdminPageView",
role: ["admin"],
url: "/adminPage"
}
}
ちなみにですが、JSONの内容からビューのコンポーネントをrequireしてreact-routerのルーティングを動的に構築するために、当時はwebpackを使わざるを得なかった。今だとbrowserifyでも、require-globifyとかbulkifyあたりを使えば同様のことができると思う。
CRUD操作が多い
マスタメンテみたいなCRUD操作中心の画面がそこそこある。対象となるデータが違うだけで、どれも似たような画面になる。というわけでこれもコンポーネントとして汎用化しておいた。CRUD操作ごとに必要なFormのスキーマ情報をprops
で渡すイメージ。このコンポーネントの中では前述のFormコンポーネントを使っており、Formを指定したスキーマ定義で自動的に生成している。
今回のアプリでは子画面(実際は擬似モーダル)を開いてCRUD操作を行うパターンが多いため、このコンポーネントに関しては独立して動作するように内部でFormデータのStateを管理するようにしていた。利用する側(親コンポーネント)はCRUDコンポーネントを貼り付けるだけで使えるようにしていました。
render() {
return (
<div>
<CRUD
create={...}
read={...}
update={...}
del={...}
/>
</div>
);
}
表形式でデータを表示
表形式というとExcelみたいな高度なUIを求めてくるところもあるが、今回はフィルタリング、ソート、ページング、ヘッダの固定くらいができれば十分だった。データ量は多くて1万件くらい。これくらいの量ならページ切り替えごとにデータ取得しなくてもまとめてフェッチして表示できるので余り複雑なことはしなくて済みました。
なお、ソートやページングは自前で作りこむのではなくGriddleを使用しています。基本的なソートやページング、フィルタリング機能があり、表示フォーマットのカスタマイズも可能な作りになっているのでそこそこ使いやすかった。
IEをサポート
IEをサポートといっても、最終的にはIE11のみでOKとなったのでこれは非常にラッキーだった。これがIE8と言われていたら辛かったかもしれない。一応、ReactはIE8でも動作するので対応はできるはずです。
その他
React以外に使用したライブラリ
Reactはビューのみを担当するライブラリなので、SPAを作るには足りない部分を補う必要がある。基本方針としてはOSSのライブラリがありマッチするなら積極的に使うようにしていた。
- React関連
- react-router
- react-bootstrap
- Griddle
- react-select
- react-bootstrap-daterangepicker
- flux
- lodash
- moment.js
- superagent
あたりを使っていた。もしまた同じようなプロジェクトがあれば、今なら以下に置き換えるかも。Formに関しては前述のとおり今ならライブラリが色々あるので、自前で作る部分は減りそうな気がしています。
- flux => redux
- superagent => whatwg-fetch
ビルド
ビルドはGulpを使いwebpack + Babelで。ES6(ES2015)で書いています。
所感
今回のような業務アプリであれば、Reactを使った再利用可能なコンポーネントが活きてくると思いました。ちょっとしたFormの仕様変更であればコンポーネントに渡すprops
(多くはJSONで外部に書いていたりする)を少し修正するだけで簡単に対応でき効率化できました。
また、汎用的なコンポーネントを作るメンバーはReactの深い知識(コンポーネントの細かいライフサイクルやパフォーマンスチューニングなど)は必要ですが、各画面を作るメンバーはそこまで深い知識は必要なく、アプリケーション規模が大きくなってもスケールしやすい感触を得ました。コンポーネント化しておくとそこにデザインも押し込めるので、各画面の開発ではid属性やclass属性を一切気にする必要がないのも良かった。
今後改善していきたいところも当然ながらあります。コンポーネントをprops
で色々と設定はできるものの、正しいかどうかは実際動かしてみないと分からないというのはもうちょっとなんとかしたいところ。ここはTypeScriptと組み合わせることで事前に静的チェックできる範囲が増えて、もっと幸せになれそうな気がしています。