SPAのルーティングライブラリを探していて、あまり自分好みのものがないので、自分なりのルーティングに対する考え方をまとめて、何がほしいのかを明らかにしたいと思います。
SPAにおけるURLおよびURL遷移とはなにか?
- SPA内のユーザの現在位置=URL として一次元の文字列(パス)として表現できるようにしている。
-
URLを書き換えることにより、ユーザの現在位置を書き換える ことになる。
- 現在位置を書き換えた結果をURLとしてメモする、という方式も考えられなくないけれども、実装が最終的に大変なので、アプリ内はURLを基軸にすべて画面遷移するというふうにしたほうがいいです。
- URLはメモリ内だけに保存せず、ブラウザのアドレス情報(履歴情報)としてユーザに露出して保存している
- 別にメモリ内に保存してもいいが、ブラウザのナビゲーションでページ遷移する機能をユーザが利用できなくなりUX上不便。
- 何よりデバッグがきつい
- 別にメモリ内に保存してもいいが、ブラウザのナビゲーションでページ遷移する機能をユーザが利用できなくなりUX上不便。
- RPGとかだと、セーブデータ上の主人公の現在位置は「○○村 Aマップ、座標 (x,y)」とかになってると思いますが、SPAだと業務的に意味があるようなパス情報として設計するのが大半でしょう。
ユーザの位置が複数あったり、現在位置があまりにも複雑だったりすると、上記の説はあまりうまくいかなくなりますが、レアケースだと思います。
SPAで必要になるルーティング・パス遷移とはなにか?
- 初期表示
- SPAに来た時のURL
- アプリの初期化が終わった後に初期URLについて、ルーティング処理を行う必要がある
-
history.pushState(ステート, タイトル, URL)
- どこかに画面遷移する際に主に用いる
- リンク・ボタンを押下した時、こちらを呼ぶ
- 画面のスクロール位置を保存したい場合とかは、ステートに入れる
-
history.replaceState(ステート, タイトル, URL)
- 画面のURLを書き直すだけで何も起こさない。
- たとえば、 ホーム画面や
/submenu/
のようなインデックスパスについて、特定の画面を出す場合は、そちらの特定画面専用のURLに書き換える場合に使う。
- たとえば、 ホーム画面や
- 画面のURLを書き直すだけで何も起こさない。
-
window.onpopstate
イベント- 基本的にここでURLの変更を受け取り、アプリ内の画面遷移処理、いわゆる ルーティング 処理を実施する。
- アプリ内で上記イベントのハンドラをトリガーして、画面遷移させることもある。
もっと具体的に
- 単純な遷移 (a)
- リダイレクト (a)
- URLだけ書き換えるリダイレクト
- 上記 (a) に対するアプリのルーティング処理
ルーティングはアプリにアクションオブジェクトを投げつける行為である
たとえば、 /users/9999
というパスに遷移した時、下記のJSONをアプリのルーターに投げつけているのと等しいと考えられます。
{
tag: "users",
userId: "9999",
}
もっとReduxっぽくいうと、こういうアクションをreducerに投げつけているのに等しいでしょう。
{
type: "route",
tag: "users",
userId: "9999",
}
一般的には、下記のフローを通っていると解釈していいと思います。
- パス(URL)を解析して、JSON様のオブジェクト(以後、JSONと称します)を得る
- 解釈不能なパスが投げられた場合は、404的なページを出すなり、デフォルトのホーム画面に飛ばすなり、ですね
- 上記のオブジェクトを、アプリにルーティング情報として送る
- アプリは上記のオブジェクトに基づいて、副作用を起こす
- ログインが必要な画面へのルーティングであれば、ログインコンソールを出して本来の画面に行かせない、とかの処理もここで行う
- CSRFの懸念があるので、いわゆる普通のサーバレンダリングアプリのGETでできる範囲を超えた処理はルーティング処理でやるべきではありません。
URLは解析するだけにあらず、作り出すこともあるだろう (bi-directional)
簡単にいうと、先程の逆写像、つまり下記のJSONから /users/9999
というパスを得る関数が必要になります。
{
tag: "users",
userId: "9999",
}
なんのために使うかというと、ユーザの現在位置を変更するためのURLをひねり出す際に必要になってきます。
つまり、 パス
⇔ パスの解釈結果
は相互に変換できなければなりません。
最終的にどういう道具や設計がSPAのルーティングに必要なのか
-
パスの取り扱いについて
- パス → JSON の変換関数
- JSON → パス の逆変換関数
- (上記について補足しておくと、パスとJSONの相互変換はアプリの内部状態に依らずに行える純粋な関数ということを前提にしています。アプリの状態に依存する不順な関数でパスの変換をしなくてはいけないとすると、設計難易度が上がると思います。)
-
アプリの取り扱いについて
- JSON をアプリに投げつけた場合の処理
- 典型的には画面遷移処理になるかと思う ログイン必須の部分はログインさせるなどのフィルタ機構もあればほしい感じ
- アプリ内で現在位置をどういうふうに表現し、それをReactなどのコンポーネントレンダリングに反映させるか
-
たとえば、下記のように現在画面にあたるアプリ内ステートを持つか?
const appState = { appScreen: "login" }
-
- JSON をアプリに投げつけた場合の処理
-
ブラウザの履歴取り扱いについて
-
history.pushState(...)
/history.replaceState(...)
させるモジュール- Reactなら
<Link href={"/some/path"}>リンク</Link>
みたいなものを書くとデフォルトでワイヤされてる履歴移動方法がDIできると良い
- Reactなら
-
window.onpopstate
のイベントハンドラ- きちんと webpack とかでHMRしても動くようにしたい
-
巷のルータについての文句
- ReactなどUIライブラリとルーティングは全然別の関心事なので一緒にしないでほしい
- ルーティングが発生するということは、アプリへの何らかのアクション(入力)が生じたのと同じであって、UIコンポーネント層でなにかする話では一切ないと思っている
- ブラウザのURL欄とは全く別にアプリ内で画面コンポーネントのパスのようなものを持っておき、それを表示するために使うのならアリな気はするが、あまりにも大げさすぎる仕組みだから、結局は無駄だろう
- 型安全になりたい
- 双方向(bidi)でパスを取り扱えるといい
- URL欄とアプリのステート(の一部)を同期させればいいというアイディアは長期的にうまくいかないと思う
- 上記みたいにしていたとして、やりたい処理ができるのなら文句は言わないですが…
その他の話題
- ユニバーサルルーター
- ブラウザ以外で動く、サーバサイドレンダリングで動く場合はルーティングはどうなる?
- ルーターのセキュリティ
- 途中でも書いたけど、CSRFの温床になるので、ルーティングだけで重要な処理は絶対に動かさない。あくまで表示系統だけ。
- ルーターなんて多少不自由なほうがいい。決まりきったことをやれば問題ない。
- routerはラウターと発音してもよい