HypernovaはSSRを実現してくれるライブラリ
個人で作ってるサービスで使っている
詳しくは一週間前くらいにRails DMで話したのでそちらを参照してください
hypernovaはhypernova/serverが落ちていてもコンテンツが表示されるようにclient側で実行されるようになっているのですが、これがうまく動いてなかったのが今回の問題
スペック
- hypernova: v2.2.6
- hypernova-ruby: v1.3.0
- hypernova-react: v2.1.0
変更前の状態
今回は画面上部に配置するナビゲーションバーをReactでコンポーネント化しているのでそれを例にする
ディレクトリ構成としては次のような感じ
webpackではroot directoryとしてapp/javascript/components
が指定されている
app/javascript/components └── layout └── navbar.tsx
import * as React from "react"; import { renderReact } from "hypernova-react"; class NavBar extends React.Component<{}, {}> { ~~~~ } export default renderReact("NavBar", NavBar);
view側で呼び出す時は次のように呼び出している
render_react_component('layout/navbar.js')
hypernova server側はこのようになっている
const { environment } = require('@rails/webpacker') const hypernova = require("hypernova/server"); const requireFromUrl = require("require-from-url/sync"); const config = environment.toWebpackConfig() hypernova({ devMode: process.env.NODE_ENV != 'production', port: 3030, getComponent(name) { if (config.devServer) { const t = new Date() const url = `http://${config.devServer.public}${config.devServer.publicPath}ssr/${name}?t=${t.getTime()}` return requireFromUrl(url).default } else { return require(`${config.output.path}/ssr/${name}`).default } } });
この構成だとサーバーサイドではレンダリングされるけどクライアント側ではレンダリングされない
どう解決したのか
まず何故動いていなかったのかを出す前にどうしたら解決できたかを書く
コンポーネント側でexportする部分を次のように直すことでサーバーでもクライアント側でもレンダリングされるようになりました
-export default renderReact("NavBar", NavBar); +export default renderReact("layout/navbar.js", NavBar);
なぜこれで解決するのか?
ここからはなぜ↑の解決方法で解決したのかをhypernovaの実装を見ていく
renderReact
まずコンポーネント側でexportする際に使っているrenderReactメソッドは次のように実装されている
https://github.com/airbnb/hypernova-react/blob/master/src/index.js#L6-L32
中身を見てもらうと分かるがserver、cliet側で呼び出された時のレンダリング用の処理が定義されている
server側ではReactDOMServer.renderToString
をclient側ではReact v16から追加されたReactDOM.hydrate
があればそれを使い、なければReactDOM.render
を使うようになっている
どちらを実行するかは次の部分で定義されているようにwindow
オブジェクトがあるかないかで出し分けているようです
https://github.com/airbnb/hypernova/blob/master/src/index.js#L85-L89
render_react_component
view側で使用するrender_react_component
はhypernova-rubyが提供するhelperで役割としてはviewの中に複数のコンポーネントが存在する場合にそれらを識別できるようにマッピングして__hypernova_render_token[0]__
のような文字列をviewに出力します
https://github.com/airbnb/hypernova-ruby/blob/master/lib/hypernova/controller_helpers.rb#L38
後はコントローラーでaround_filter :hypernova_render_support
を追加していると思うので、これが実行されることによってrender_react_component
によって追加されたコンポーネントをまとめてhypernova/serverにリクエストして帰ってきたHTMLを__hypernova_render_token[idx]__
とリプレイスして返している
今回の問題だった点
今回は呼び出し側で render_react_component('layout/navbar.js')
と定義していたので、生成されるdomは次のようになる
<div data-hypernova-key="layoutnavbarjs" data-hypernova-id="11111111-22222-33333-4444-534a36f354f9">~~~~~</div>
server側で実行される場合はrender_react_component
によってマッピングされたものを元にreplaceしていくので問題がないがクライアント側で実行される際には生成されたdomのdata-hypernova-key
を探してレンダリングする処理を行う
https://github.com/airbnb/hypernova/blob/master/src/index.js#L71-L83
そのため今回はrenderReact("NavBar", NavBar)
と定義していたのでdata-hypernova-key='NavBar'
を探してにいってしまってその要素が見つけられずクライアント側ではレンダリング出来ていなかった
最後に
今回はクライアント側で実行されなかった問題を調査して解決までいった