15 min/d

ぼうずやのにっき

Cycle.js で server-side rendering につまずいた

Cycle.js で server-side rendering しようとしてつまずいたことを書く。

Cycle.js は RxJS および virtual-dom をつかった JavaScript framework のひとつ。副作用を driver に切り出す点などが特徴だ。SSR: server-side rendering は SPA: single page application を使う際に server-side で pre-rendering しておくこと。 rendering 済みの HTML を返せるので SEO 面で強くなったり、client-side で通信完了から初期描画までの時間を稼げるなどの利点がある……と考えている。余談だが将来的にすべての SPA は SSR に対応すべきだとぼくは考えている。つまり、ぼくは SSR を必須の要素として考えている。

今回の目的は Cycle.js の実用性の検証として SSR をためすことだ。

以下の文章を要約すると、ぼくの結論は「 実装によるが Cycle.js の cycle の構造や driver を括り出す構造は SSR に向かない」ということだ。もうすこし書くと「server-side の request & response の対は Cycle.js の 1 cycle に対応すべきだけど、一方で 1 cycle で入出力をできないために Cycle.js では対応するのが難しい」。あるいは「 cycle できない Cycle.js に何の意味がある?」ということだ。どこかで間違っている可能性はあるけど、そんな風に感じている。

ここから長いので読まなくて良い。

実装はひどく中途半端なので公開していない。

まずぼくの理想は Cycle.js を model-view-intent でつくる。具体的には次の形でつくりたかった。

const main = (sources) => view(model(intent(sources)));
const drivers = { /* ... */ };
run(main, drivers);

さらに言うと model の型は次のようにしたかった。

const model: (actions: { [actionName: string]: Observable<any> }) => Observable<State> = /* ... */;

ここまでがぼくの理想形だ。

この理想形で SSR に関係なく存在する問題について書く。

実はこの形だと実装は面倒になる。 modelState を流す Observable だけれど、view で各 driver 別に値を流す量を制御しないといけない。たとえば 標準の HTTP driver は値が流れるたびに HTTP request を投げる。 標準の DOM driver は値が流れても Virtual DOM のおかげで DOM は無駄に更新されないのに、だ。つまり HTTP driver への出力には一枚岩の State からうまく重複した値を流さないように情報を切り出す必要がある。これは実に面倒だ。

SSR においてもこれと似たような値を流す流さない問題がある。

server-side では request を受けるとそれに対応する response を一度だけ返す必要がある。一度だけ、それも期待する形で流す。これが難しい。

1 cycle なら値を増やさないかぎりは一度だけ流れるだろう。では 2 cycle ならどうだろう。HTTP driver で外部と通信したらどうなる? 途中で描画されないように工夫が必要だ。

server-side の response で 2 回送ることは許されない。ある入力に対して確実に 1 回だけ、それも期待する形で返さないといけない。

「期待する形」を強調するのは、期待した State を 1 回の cycle でつくれるとは限らないからだ。

Cycle.js では HTTP driver で 1 周して DOM driver へ再度流すなど Cycle.js での cycle を繰り返すことが多い。つまり何度も回りながら State をつくることはありえる。じゃあ、間に HTTP request を挟むとき、途中に vtree$ を流してはいけないのか? State を流してはいけないのか。Virtual DOM で楽になる要素のひとつは一体どこに行ったんだ。

こういうものを「べき等性」というのだろうか。何度実行しても大丈夫になっていないし、何度も実行しなければ期待した State をつくれない。

良い案がある。途中で入出力すればいい。RxJS なら非同期処理 (Promise) をはさんでも容易に flow 制御できる。でも、それ Cycle.js じゃないじゃん。

他の方法として、最初に挙げた理想形を捨てる手もある。 modeldriver ごとの Observable を操作させる手もある。でも model が入出力 (driver) を意識しているってそれでいいのか。

ここまで考えて、ぼくはある結論に至った。server-side の request & response の対は Cycle.js の 1 cycle に対応すべきだけど、一方で 1 cycle で入出力をできないために Cycle.js では対応するのが難しい。

cycle できない Cycle.js に何の意味がある?