既存のWebアプリをReactで書き換えた話(+ES6の困った点)
本当は1ヶ月前に下書きを書いた記事なのですが、
忙しくて見直す時間がなく、だいぶ遅くなってしまいました(´・ω・`)
その間にバグfixを進め、一応安定動作していますし、
それもふまえた内容に書き換えていく方向で・・・
相変わらずメンテと拡張を続けている
「チェンクロパーティーシミュレーター」(以下「ccpts」)ですが、
先日Reactでほぼ全体を書き換えました(`・ω・´)
ちょうど1年ほど前、私がReactを初めて意識した際に、
一度やろうとしたことはあるのですが、
いろいろな理由*1があって諦めてました
しかし、昨年から仕事でNode.jsを扱うように*2なり、
先日はccptsのロジックをFRPで整理するのに成功したりと、
今ならいけるんじゃないか・・・と思い、今回試してみたわけです
React(+flux)についてはいくらでも素晴らしい解説が見つかりますので、
今回は「すでに動いているものをReact化するまでの段取り」と
「その過程で考えたこと」を書いていきます...φ(・ω・`)
全体の段取り
React化に以前失敗しているのもあり、事前に全体の段取りを立てて臨みました
(というより、その段取りが見えたから今回進めたというか)
まず、react-railsですが、これを使うとjsxのサーバサイドレンダリングが可能になります
初期の段階ではerbで書かれたHTMLの構造を、ほぼそのままjsxにコピーしていき、
サーバでレンダリングさせることで、
(jQuery等から見たときの)HTMLの構造はほぼそのままになります*3
erbをjsxに書き換えただけにはなりますが、初期の段階ではそれでも十分ですし、
サーバでのレンダリングをやめることが最終目標になるわけです
ccptsのメインロジックはブラウザで実装されており、
erbで動的な出力は最低限になってはいますが、
それでもReactを前提にするといろいろいじる必要は出てきます
(余談ですが、元のviewがHTML埋め込みのerbだったからこそ使えた手で、
haml等を使っていたら全部書き直しでしたね・・・
別にhamlが悪いわけではなく、結果的に、ですけども)
react-railsのおかげで、部分的に置き換えていくというのが容易になったので、
動的制御のない部品をどんどんjsxに置き換え、
画面で動作確認する・・・というフローが可能になりました
以前は上から作っていったので、終わりが見えずに心が折れましたが、
下から部品単位で置き換えていけば、「動く状態」を維持できるため、
「いつでもリリースできる」という安心感もあります(`・ω・´) *4
同様に、flux等のstate管理フレームワークは考えませんでした
そういった仕組みを導入すると、また上から全部考え直しになるので、
どんな形であれReact化してから考えればいいかな・・・と
(flux実装の一つであるreduxを見て今回のReact化を検討したり、
ES6の使用を決めたのは事実ですが・・・)
実際の進め方
1. ほぼコピペでjsxにコードを移動
先ほども書いたように、erbにロジックはほぼ入ってなかったので、
ガンガンとコピペしていくだけです( ゚д゚)o彡゚
コピペで引っかかるのは「class="hoge"」を「className」に変えるとか、
「for="piyo"」と「htmlFor」に変えるとか、
「inputやbrタグにも閉じタグが必要」とか、そんなところでしょうか
ほぼ機械的に変換できますし、ChromeのReact拡張を入れておけば、
consoleで丁寧にwarningが出るので、順番に潰していけばOKです
erbでなにか埋め込んでいる箇所は、react_renderの引数にその値を埋め込んで、
jsx側のpropsで受け取るようにしておきます
# LatestInfoはReactのクラスでChangelogはRubyのクラス # prerender: true によりサーバサイドでレンダリングされる <%= react_component('LatestInfo', {info: Changelog.latest.as_json}, prerender: true) %>
jsxに移す際に「クラス名」をつけることになるわけですが、
名前をつけるというのはそれが何者であるのかを明示する、ということであり、
これでViewの構造をもう一度見直すことができたのはいい感じでした
ただし、これはあくまで「補助輪」です
この段階ではreact_componentを使いまくりますが、
その後はむしろ、react_componentを使わない=pureなJSに修正していくわけです
2. 動的フォームでstateを使用
これで全体をいったんjsxに書き換えたのですが、
問題になるのが動的に構造を作っている箇所です
ccptsの場合、検索フォームがその一つで、
大項目を選ぶと、小項目がそれに合わせて作られる、という仕組みになってます
(アビリティやスキル検索のところなど)
しかし、jQueryでイベントを発火させても、
書き換えたDOM構造をまたReactが上書きしてしまうのですΣ(゚Д゚)ガーン *5
そこで、この検索フォームをReact化の第一歩として、
少しずつロジックを実装していくことに
最初はスキルとかアビリティとかの単位で分離し、
その中でstateを管理する・・・という形でいったん実装したのですが、
それで困るケースが二つ出てきまして
一つが「条件のリセット」です
それぞれの項目を初期状態にするわけですが、
値を各component単位で管理しているので、
どうやってresetしてもらうか、という問題が
もう一つが「初期クエリがある場合」です
ccptsのデータベースモードは、検索結果を共有する仕組みがあり、
表示時点で指定された検索クエリをformに反映させないといけません
リセットは「空にする」ことで対応できますが、
初期状態は不定なので、単純には行きません(´-ω-)
そこで、Reactの設計らしく、親である検索フォームで全stateを管理し、
子である各フォームにpropとして伝える、という仕組みを採用しました
このとき、各フォームごとにハンドラを親で定義して、
子に引き渡す・・・というのが面倒だったので、
親でstream(Bacon.Bus)を用意しておき、子は変更された値をpushする仕組みにしました
// from children this.notifier = new Bacon.Bus() this.notifier.onValue((s) => { // 子から"{job: 'F'}"みたいにpushしてもらう this.setState(s) })
ここまで書いて気づいたのですが、この「stateの塊」って、
実はほぼ「検索クエリ」なんですよね
FRP化した際、検索ボタンを起点にして、クエリを構成し、
APIに投げて結果からviewを生成・・・というstreamが定義されてました
ということは、検索フォームのstateを検索のstreamに乗せてやれば、
そのまま既存のstreamから検索が動くってことなのです
「クエリの構成」という部分が、フォームのparseではなく、stateで終わるってだけで
逆に、外部からのクエリを検索フォームのstateに反映させれば、
フォームの状態を自由に変えることも可能です
つまり、「検索フォーム」というcomponentを、
「クエリを受け取る口」と「クエリを吐き出す口」を持った、
独立した部品としてみなすことが可能です(`・ω・´)
flux実装であるreduxは、アプリ全体で一つのstateを管理する仕組みのはずですが、
FRPから入った私にとっては、「通知用streamが存在する独立した部品」の方が、
概念としてしっくりきたわけです
RxJSとReactを組み合わせたフレームワークに、
Cycle.jsというのがありますが、それに近い形です
ここまで徹底してはいませんが、考え方は近いです
「何かあったらstreamに流すのでよろしく(`・ω・´)ノ」というのが、
まさにObservableな設計であり、個々の関係を切り離す鍵になってます
3. react-bootstrapの部品を使う
これで完全に「つかんだ」ので、
他の動的な部分ももりもりと進めていこうと思ったのですが、
次に引っかかったのがmodalの扱いです
元々jQuery経由でbootstrapのJSが制御してましたが、
modalの中に状態がある、というケースがありまして、
検索フォーム同様、そこがReactだとうまくいかないわけです(´-ω-)
そこで、このタイミングで「react-bootstrap」を投入することに
リファレンスを見てもらえばわかりますが、ModalやPaginationなど、
動的な処理をしてくれるcomponentがたくさんありまして、
これでだいぶ構造が整理されました(`・ω・´)
Paginationなんか自前で書いてましたが、
この部品を使えば値を渡すだけですからね・・・
以前はここまで整備されていなかったと思います
一方、無理に全ての部品を置き換える必要はなく、
ほとんどはdiv+classでけりがつきますし、
部品にpropが足りないので、あえてdiv+classのままにしたところもあります
4. データベースモードがReact化
あとは、何か通知が必要ならstreamを定義し、
streamを整理していくうちに統合化され・・・と、
開発を進めていったところ、データベースモードがほぼReact化されました
データベースモード : Get our light! - チェンクロ パーティーシミュレーター
そもそも「データベースモード」は、「PT編集モード」の検索部分を切り出し、
見せ方だけ変えたものなので、これで作られた部品は、
ほぼそのままPT編集モードに使いまわせます
(まさに「コンポーネント指向」かと)
この時点で、「いつでもリリース可能」という状態を捨てて、
データベースモードの部品から共通部分を取り出しつつ、
PT編集モードをトップダウンで作っていくモードに
5. jQueryとの連携
Reactの話を見ていると、「jQueryを捨てる」的な話がよく出てきますが、
ccptsに関していえばそれは不可能です
なぜなら、PT編集モードはドラッグアンドドロップが必須で、
それらを簡単に使える仕組みがReactにはまだないからです(´・ω・`)
あと、アニメーションに関する仕組みも貧弱すぎます
一応、react-bootstrapにはFadeの部品がありましたが、
いまいち使い方がわからないし、jQueryと同じ感覚でもないです
そこで、どうやってReactのVirtual DOMと、
jQueryのDOMを連携させるか・・・ですが、
refという仕組みでDOMを生で抜き出すことが可能です
<div className={`${a.jobClass} summary-size arcana`} ref={(div) => { // このdivタグにドラッグのハンドラをセット this._div = div this.setDraggable(a.jobCode, this.props.code) }}>
同じように、jQueryのfadeInやhideもセットできるので、
Fadeのcomponentは捨てまして、全部jQueryに切り替えました
ただし、「どのタイミングでアニメーションを適用するか?」は、
Reactのライフサイクルを把握してないとうまくいきません(´・ω・`)
「componentDidMountのとき」だとか、
「setStateが完了したあとのcallback」だとか、
適切な見極めが必要になります
あと、ブラウザや環境依存でうまく動かないケースもあります
Safariだけ挙動がおかしいケースがあったりで、だいぶ悩みました*6
それでも、Reactの仕組みで自前のアニメーションを適用するよりはるかに楽です
Reactの仕組みは難しすぎ、私には無理です(´-ω-)
6. ES6の困った点
jsxのコードを全部ES6で書いたので、
結果的にViewのコードはCoffeeScriptからES6に変わったのですが、
modelや自前のライブラリはCoffeeScriptのままです
一度は全部書き直そうと思ったのですが、
同じロジックのはずなのに動作しないケースがあったりと、
謎が多かったので戻しました
動いている部品を無理に書き換える必要はないわけですし
Viewは作り直しになったから変える動機があったのであって
そもそも、CoffeeScriptと比較して、ES6は以下のような困った点があります
後置ifが使えない
「unlessが書けない」というのも困るのですが、
それ以上に困るのがこれです
普段Rubyを書くときもそうですが、
私はメソッドの頭にガード条件をよく書きます
someMethod = (a) -> return unless a # aが存在しなかったらreturn # aを使う処理
これがES6だとこうなります
someMethod(a) { if (!a) { return // aが存在しなかったらreturn } // aを使う処理 }
一つくらいのガード条件ならいいのですが、
複数並ぶとさすがにうっとうしくなってきますщ(゚Д゚щ)
とはいえ、これだけでES6からCoffeeScriptに戻すってのは、
現在の状況を考えるとあり得ないのですが、最大の問題は次です
クラスのプロパティも定数も定義できない
これに関してはReactの開発側も困っているようですが、
ES6のclassで定義できるのは「メソッド」だけで、
「プロパティ」や「定数」や「privateな変数」が書けないのですΣ(゚Д゚)ガーン
RubyのActiveRecordのように、
Reactでもpropsの型をチェックする仕組みがあるのですが、
当然、classで共通なので、classのプロパティとして定義したいわけです
しかし、ES6ではそのような記述が許されていないため、
classを定義したあと、classに代入するという、
美しくない方法を使うしかありません(´-ω-)
もっとひどいのが定数です
見かけ上、「定数のように見えるもの」は書けます
static get hoge() { return _hoge // これをどこに定義する?? }
「get / set」と使うと、かっこなしでのメソッド呼び出しや代入が可能ですが、
「値を格納する変数」はどこに書けばいいのか・・・というのが問題です
ただの定数ならclassの後で代入でもいいですが、
その値をclass内のメソッドで使いまわしたいとすると、
class定義の外で変数を定義しないといけません
でも、それってグローバル環境を汚染しているわけで、
「何のためのclassなのよ(ノ゚Д゚)ノ彡┻━┻」という気分に
他でもできないならいいのですが、CoffeeScriptはもちろん、
TypeScriptでもできるし、ES7には入ってるわけですからね・・・
結局、この仕様が気に入らず、babelでの変換もいまいちうまくいかなかったので、
今回はmodel等はそのままにしています
7. 最終調整とReact化の弊害
いろいろと壁はありつつも、なんとか大枠での実装が完了し、
Viewの実装はほぼ完了し、関連ライブラリも全てnpmに移行しました
そこで、テスト環境で実機での確認に移ったのですが、
ブラウザ依存の問題やバグは別として、以下のような問題が発覚しました
・precompileがめちゃくちゃ遅い
デプロイ時に「rake assets:precompile」を呼ぶ際、
CoffeeScriptとは比較にならないレベルで時間がかかります(lll゚Д゚)
Reactのjsxはもちろん、ES6も変換しないといけないし、
Viewで使うJSのファイル数が跳ね上がったから仕方ないのですが、
いくら本番VPSのCPUが弱いからといって、数分待たされるのは怖いです
まあ、precompileが完了してしまえば、
実際の呼び出しで(変換により)待たされることはないので、
viewのコードを変更してデプロイした場合に限られるのですが
頻繁にデプロイするとか、規模が膨大の場合は、
ローカルでprecompileしておいて、
出来上がったファイルをgitで管理する・・・なんても必要かもしれません
・JSの読み込みが遅い
precompileを経由し、全て結合したファイルにアクセスするとはいえ、
JSのファイルサイズは大きく、ロードに結構かかります(lll゚Д゚)
一度キャッシュされれば許容範囲ではあるものの、
それでも以前の単純なviewに比べ、待たされる感があります
以前は枠をサーバでレンダリングし、
中身を非同期通信で呼び出す仕組みだったのでまだ良かったのですが、
Reactだとアプリ本体が全体を描画するので、何も表示されない時間があるのです
JSの読み出しと解釈に時間がかかるって問題を、
JSの文脈(jQuery等)で解決ってのは当然できません
なんとかして別の手段でごまかす必要があります
そこで、サーバ側で「loading」って画面を描画しておき、
Reactの描画が完了した時点で消す、という手段を使いました
これだけでも「何かしてるんだな・・・」という感じが出ます(`・ω・´)
これからのお話
そんなこんなで、やっと安定して稼働し始めたccptsのReact版ですが、
最後にやってみた感想とか今後の話などを...φ(・ω・`)
仕事に役立つのか?
少なくとも、今の環境では使わないかな・・・と思ってます
基本、私の仕事はAPIとかサーバサイドの構築であり、
今回の主目的はES6とか非同期処理の記述に慣れるのが目的でした
例えば、システムの管理画面にReactって話は高コストすぎるかと(´-ω-)
もちろん、「未来の最適解」を「プライベートで」模索しておくことに大きな意味があり、
間接的には仕事に反映はされていくはずではあります
少なくとも、仕事でNode.jsを書く際にES6を使おうとは思いました
・・・まあ、CoffeeScriptの方が気楽なんですけどねΣ(・ω・ノ)ノ
reduxをどうするか?
fluxというか、Reactと組み合わせて使うフレームワークとして、
reduxが話題になっており、当初は私の最終目的もそれであり、
だからこそES6を試した・・・という事情はあります
ただですね・・・FRPとの組み合わせで実装してみると、
「componentの入出力だけお見せするので、好きな時に通知をください(`・ω・´)ノ」の方が、
stateを一箇所で集めるよりも、独立性が高まっている感じがして、しっくりくるんですよね
むしろ、検討するならCycle.jsかな・・・とも思いますが、
そこまでくるともはやReactですらないので、やりすぎかなと(´-ω-)
react_on_rails
完成させてから気づいたのですが、こういう仕組みがあるようです
クライアント部分を独立した形で開発し、
コンパイルしたjsをassetsに配置するという一連の流れを、
うまいことフレームワークとして自動化してくれるというものっぽいです
(最近よく議論されている方式のフレームワーク化ですね)
おそらく便利だと思いますが、あまり情報が出てないのと、
あまりにフルスタックすぎて、知らない人が一から触ると混乱しそうです
慣れた人がショートカットするにはよさげ
そんなこんなで、自分のメモという意味もこめてだいぶ長々と書いてきましたが、
お仕事でがっつりRailsのお仕事に戻ったのもあり、
しばらくはccptsのview周りのいじることはないかな・・・と
(もちろん、機能追加は別として、フレームワーク的な意味で)
次はAPIをRails5に書き換える予定ですが、
RC1はまだですかね・・・(´・ω・`)
*1: 私自身がJSに慣れてない、react-bootstrapが今ほど整備されていない・・・等
*2: これを書いた時点では扱う予定でしたが、現在のお仕事だと使わない予定ですΣ(゚Д゚)ガーン まあ、また別なところで使うとは思いますが・・・
*3: より厳密に言うと、専用のdata属性付きタグが出力され、react_ujs.jsがそれをフックしてreactのcomponentに置き換えるという仕組みなので、静的なHTMLが直に出力されるわけではありません・・・が、jQueryでイベントハンドラをセットする側からすると、ほぼ同じに見える=既存の書き換え不要、というだけです 詳しくはreact-railsのREADME参照
*5: jQueryが「本物のDOM」をいじる -> Reactの仮想DOMはそのままなので、reactが差分を検知 -> 本物のDOMが上書きされる・・・という感じだと思いますが、あってますかね?
*6: jQueryのshowのあと、テーブルが再描画されないという問題だったので、streamに「再描画」メッセージを流して、同じstateで再描画させる手段で解決しました(`・ω・´)