React+jQuery+RailsのSPAをサーバサイドレンダリングに移行した件(その2:ブラウザ依存排除編)
というわけで、前回の続きです...φ(・ω・`)
<サイト>
ccpts.parrot-studio.com
<修正したコード>
github.com
前回は「サーバサイドレンダリング(SSR)」の概念的な話と、
そこから導かれる設計の概要、そしてSSRに移行するための段取りについて書きました
- ブラウザ系オブジェクトの排除(とりあえず実行時エラーを消す)
- 「仕様上の正しい動作」になるように修正
- インフラの調整
今回はこの一番最初、「ブラウザ系オブジェクトの排除」からやっていきます
STEP1:実行時エラーの消去
とにかくまずは「エラーが出ない」「何か画面が表示される」を目指します
前回、windowオブジェクトを排除すればエラーにならないのでは・・・という推測しましたが、
これを立証するのが最初のステップになります
(1) 二つの領域のjsと、それをつなぐProxy
具体的なRailsの話であり、前回書いた話になりますが、
Railsが扱うjsはざっくり2種類あります
Railsのassetsに属する「application.js」と、
webpackでまとめられ、SSRにも使われる「Reactを主体にしたjs」
(以下、アプリ名にあわせて「ccpts.js」と表記)です
前者は「app/assets/javascripts」、後者は「app/javascript」に存在し、
それぞれ別なルートでまとめられて一つになります*1
「application.js」は(HTMLのlayoutを経由して)クライアントのみで読み込まれますが、
「ccpts.js」サーバとクライアント双方で実行されます
この「ccpts.js」にwindowオブジェクト等が含まれるとまずいことになります(´-ω-)
ならば、windowに触れるコードをapplication.jsに追い出してしまおう・・・ということです
まず、windowとかjQueryとか、ブラウザ系のオブジェクトに触るコードを、
「Browser」というオブジェクトに閉じ込めてしまいます
// app/assets/javascripts/browser.js の抜粋 // webpackを通さないので、TypeScriptではない // クラス的に振る舞うグローバルオブジェクトを定義 Browser = {}; // window.location Browser.thisPage = function () { return window.location.href; } // window.confirm Browser.confirm = function (mes) { return (window.confirm(mes)); } // jQueryに依存する処理の例 Browser.addSwipeHandler = function (div, callbackLeft, callbackRight) { if (!div) { return; } $(div).swipe({ swipeLeft: (function (e) { e.preventDefault(); callbackLeft(); }), swipeRight: (function (e) { e.preventDefault(); callbackRight(); }) }); }
元々React化した際、jQueryなどのライブラリは「application.js」に追いやられているので、
これでwindowに触るコードとライブラリが全て「application.js」に分離されたことになります *2
しかし、このままではReact側で生成したDOMと、これらの処理が連携できません
そこで、「クライアントサイドではBrowserに処理を委譲し、
サーバサイドでは適当な値を返すオブジェクト(≒クラス)」として、
「BrowserProxy」を定義します
// BrowserProxyの抜粋 // 他のコードから追い出されたdeclare // ここにしかdeclareがないので、他のコードに外部依存がないことを保証できる declare var window declare var Browser export default class BrowserProxy { // windowオブジェクト(とBrowserオブジェクト)の有無を判定 public static isWindowDefined: boolean = (() => { if (typeof window === "undefined") { return false } if (typeof Browser === "undefined") { return false } return true })() // windowがなければ特定の値を返す例 public static thisPage(): string { if (BrowserProxy.isWindowDefined) { return Browser.thisPage() } return "/" } // windowがあるときだけhandlerをセットする例 public static addSwipeHandler(div, callbackLeft, callbackRight): void { if (BrowserProxy.isWindowDefined) { Browser.addSwipeHandler(div, callbackLeft, callbackRight) } } }
このように、windowやBrowserが存在する(≒クライアントサイド)では、
まんまBrowserに処理を委譲していますが、
Browserが存在しない(≒サーバサイド)環境では何もしないか、適当な値を返しています
すでにTypeScript化しているので、外部参照している箇所は
「declare $」とか「declare location」とか、declare宣言されているので、
そこをBrowserとBrowserProxyにどんどん切り出していきます*3
呼び出す側はこんな感じです
// 関連する箇所だけ抜粋 export default class DatabaseTableArea extends ResultView<ResultViewProps> { private arcanaTable: HTMLTableElement | null = null // componentをマウントする時にハンドラをセット(ただし、クライアントならば) public componentDidMount(): void { Browser.addSwipeHandler( this.arcanaTable, this.handleLeftSwipe.bind(this), this.handleRightSwipe.bind(this) ) } }
この段階では、SSRと無関係に、ただコードを整理しているだけですので、
処理を切り出したらSSRなしで動作が変わらないことを確認・・・という感じで進めます
(2) 暫定的Cookie対応
ここまでの作業が終わったところで、試しにSSRに切り替えたところ、
「window not found」ではないエラーに変わりました
これはこれで一歩前進ですが、エラーの内容を調べたところ、
Cookieを読んで初期化するところで落ちてました(´-ω-)
この時点ではサーバサイドJS実行系におけるCookieの扱いがわからなかったのですが・・・
・・・ということで、サーバサイドでCookieのオブジェクトをparseし、
Reactの初期値として構造を丸ごと渡す・・・という強引な手にΣ(・ω・ノ)ノ
これはあくまで一時的な処置で、次のSTEP2で適切に置き換えられるのですが、
とにかくこの段階では「エラーを消す」(そしてBrowserProxy方式の妥当性を検証する)のが
最優先なので、無理矢理な手段をとりました
この修正により、ついにSSRでHTMLが吐き出され、
それっぽい画面が表示されました・・・が、
それは新たな戦いの始まりでしかなかったのです・・・
今回のまとめと次回予告
今回は実際の例として、Railsの文脈での作業について書いてみましたが、
一般的なポイントは以下です...φ(・ω・`)
- jsを2つに分離する
- 「クライアントでだけ実行されるもの」と「サーバ・クライアント双方で実行されるもの」
- クライアントでだけ実行されるjsに、ブラウザ系オブジェクトを参照するコードを集める
- ブラウザ系オブジェクトの処理を代行するProxy層を用意する
これで「表示」まで進むことができ、
SSR自体がいけそうな気がするレベルにはなったのですが、
進めていくと、「クライアント」と「サーバ」を別な形で意識させられることになります
ということで、次回「STEP2:仕様変更編」へ続きます(`・ω・´)
*1: 前者はsprockets、後者はwebpacker
*2: どこかで書いたと思ったら、ちゃんと書いてなかった気がするけど、とりあえずこのあたり https://parrot.hatenadiary.jp/entry/2016/02/28/113310
*3: declareについては以前の記事参照 https://parrot.hatenadiary.jp/entry/2018/08/29/171654