RailsでReactをサーバサイドレンダリング（use Hypernova by airbnb）


## はじめに

Railsにも対応しているJavaScriptのサーバサイドレンダリング用ライブラリ[airbnb/hypernova](https://github.com/airbnb/hypernova)が、airbnbからリリースされました。

この記事では、シンプルなRailsアプリを構築し、これまたシンプルなReactコンポーネントをサーバサイドレンダリングで表示するまでのチュートリアルを解説します。

ReactによるSPA（シングルページアプリケーション）も増え、Google等の検索エンジンでもある程度はページ内容を解釈してくれるらしいですが、サーバ側でHTMLをレンダリングして出力するサーバサイドレンダリングを利用すると、より正しくページ内容を解釈してもらえます。

## Hypernovaとは

> A service for server-side rendering your JavaScript views

Hypernovaは、JavaScriptコンポーネントをサーバサイドでレンダリングするためのサービス（ライブラリ群）です。

### Hypernovaのアーキテクチャ

[公式の図](https://github.com/airbnb/hypernova/blob/master/docs/hypernova-how-it-works.png)が分かりづらいので、シーケンス図にしてみました。
![](https://qiita-image-store.s3.amazonaws.com/0/6368/ab05a8cf-9e86-881e-fa90-fbbb1c7ee704.png)

図中のHypernova関連の登場人物と、代表的な役割は以下の3つです。

| 登場人物 | ライブラリ | 役割 |
|:--------|:---------|:----|
| Hypernova/ruby | [airbnb/hypernova-ruby](https://github.com/airbnb/hypernova-ruby) | HypernovaのRubyクライアントでRails用のFilterやHelper methodを提供します |
| Hypernova/server | [airbnb/hypernova](https://github.com/airbnb/hypernova) | Hypernovaのサーバサイドレンダリングを担うエンジンで、コンポーネントを読み込んでHTML化します |
| Hypernova/react | [airbnb/hypernova-react](https://github.com/airbnb/hypernova-react) | Reactコンポーネント用のバインディングで、Hypernova/server用のインタフェースを提供するとともに直接ブラウザで読み込まれても大丈夫な機構(fall-back)を備えます |

Hypernovaのコアは`Hypernova/server`と`Hypernova/react`の2つで、前者がサーバサイドレンダリングを担い、後者が`ReactDOM.render`のラッパーでReactコンポーネントをUniversal化（サーバ側でもブラウザ側でも実行可能にすること）します。

また、サーバサイドレンダリングを利用して表示したページでも、ユーザの操作によるReactコンポーネントの変化が正しく動作するよう設計・実装されています。

## Hypernovaを使ったサーバサイドレンダリングのチュートリアル

それでは、実際にHypernovaによるReactコンポーネントをサーバサイドレンダリングするRailsアプリを作成していきます。

### 環境・バージョン

```sh
# mac
MacBook Air (13-inch, Mid 2012) - OS X El Capitan (10.11.5)

# ruby/rails
% ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]
% gem -v
2.5.1
% rails -v
Rails 4.2.6

# node/npm
% node -v
v5.5.0
% npm -v
3.7.1
```

### 1. Railsアプリの作成
特別なオプションなどは不要です。

```sh
% rails new hypernova-getting-started-with-rails
```

### 2. Controllerの作成
`/welcome/index`にアクセスしたときに動作するようにします。

```sh
% bin/rails generate controller welcome index
```

### 3. browserify-railsの導入
Railsアプリ上でCommonJSを動作させるために`browserify-rails`を導入します。
`browserify-rails`に限らず、Reactコンポーネントがブラウザで動作するようにできれば何でも良いですが、今回はもっとも簡単な`browserify-rails`を選びました。

`Gemfile`に以下1行を追加し、`bundle install`を実行してインストールします。

```rb:Gemfile
gem "browserify-rails"
```

```sh
% bundle install
Installing browserify-rails 3.1.0
```

### 4. JavaScriptの環境設定 (package.json)
以下のコマンドで`package.json`を作成し、必要なパッケージ情報などを追記します。

```sh
% npm init -y
```

```json:package.json
{
  "name": "hypernova-getting-started-with-rails",
  "version": "1.0.0",
  "description": "A sample Rails application that uses Hypernova to server render.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\""
  },
  "author": "Noriaki Uchiyama",
  "license": "MIT",
  "dependencies": {
    "hypernova": "^1.0.0",
    "hypernova-react": "^1.0.0",
    "react": "^15.1.0",
    "react-dom": "^15.1.0"
  },
  "devDependencies": {
    "browserify": "^13.0.1",
    "browserify-incremental": "^3.1.1"
  }
}
```

`package.json`が用意できたら以下のコマンドを実行しJavaScriptのパッケージをインストールします。

```sh
% npm install
```

### 5. Hypernova/serverの設定
`Rails.root`ディレクトリに`hypernova.js`を作ります。
これは、`Hypernova/server`にあたるもので、コンポーネント名を受け取りレンダリングしたHTMLを返すサーバです。内部では`express`が読み込まれています。

```js:hypernova.js
const hypernova = require('hypernova/server');

hypernova({
  devMode: true,

  getComponent(name) {
    if (name === 'MyComponent.js') {
      return require('./app/assets/javascripts/MyComponent.js');
    }
    return null;
  },

  port: 3030,
});
```

### 6. Hypernova/rubyの設定
Ruby(Rails)用のHypernovaクライアントを読み込みます。

`Gemfile`に以下1行を追加し、`bundle install`を実行してインストールします。

```rb:Gemfile
gem "hypernova"
```

続いて、Railsアプリから先ほどの`Hypernova/server`へアクセスするための設定を追加します。

`#{Rails.root}/config/initializers/hypernova.rb`として以下を作成します。

```rb:config/initializers/hypernova.rb
require 'hypernova'

Hypernova.configure do |config|
  config.host = 'localhost'
  config.port = 3030
end
```

`config.port`には先ほど`hypernova.js`に記載したポート番号を指定します。

その他、これは必須ではありませんが、エラー発生時にブラウザ上でスタックトレースが表示される設定も行います。下記をRailsアプリの設定ファイルに追記します。今回は開発環境用の`config/environments/development.rb`の末尾に追記します。

```rb:config/environments/development.rb
require 'hypernova'
require 'hypernova/plugins/development_mode_plugin'

Hypernova.add_plugin!(DevelopmentModePlugin.new)
```

これは、`Hypernova/ruby`に同梱されているDevelopment mode pluginを利用しています。

### 7. Hypernova/reactを使用したReactコンポーネント
`props.name`を受け取って表示し、クリックするとアラートを表示するだけのシンプルなReactコンポーネントを作成します。ファイルは`#{Rails.root}/app/assets/javascripts/`に`MyComponent.js`というファイル名としています。

CommonJSで動作するように関数型のReactコンポーネントを使っていますが、`React.createClass`でも、（トランスパイラを利用すれば）ES2015のクラス構文を利用した`class MyComponent exntends React.Component {`の書き方でも`Hypernova`で動作します。

```js:app/assets/javascripts/MyComponent.js
var React = require('react');
var renderReact = require('hypernova-react').renderReact;

function MyComponent(props) {
  return React.createElement('div', {
    onClick: function () {
      alert('Click handlers work.');
    },
  }, 'Hello, ' + props.name + '!');
}

module.exports = renderReact('MyComponent.js', MyComponent);
```

ここで重要なのは`renderReact`関数で、これが`Hypernova/react`の提供する関数です。第一引数にコンポーネント名（ユニークな文字列ならなんでもOK）と、第二引数にコンポーネントを受け取り、`Hypernova`用のラッパー関数を返します。

ここで第一引数に指定しているコンポーネント名が、先ほどの`Hypernova/server`が受け取るコンポーネント名と一致している必要があり、後ほど出てくるRailsのViewで呼び出される際にも指定されます。

### 8. RailsアプリからHypernova呼び出し
`Hypernova/ruby`は2つのmethodを提供します。Hypernovaによるサーバサイドレンダリングをサポートする`around_filter`の`hypernova_render_support`と、Reactコンポーネントを`props`を指定してViewに読み込む`render_react_component`です。

以下のように、使用するControllerとViewで以下のように呼び出します。

```rb:app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
  around_filter :hypernova_render_support
  def index
  end
end
```

```erb:app/views/welcome/index.html.erb
<%= render_react_component('MyComponent.js', name: 'Hypernova') %>
```

`render_react_component`の第一引数は、`Hypernova/react`の`renderReact`関数の第一引数に指定したコンポーネント名を指定します。第二引数は`props`なので、RailsからJS（Reactコンポーネント）への値の引き渡しはここで行います。

さらに、Viewで読み込むJavaScriptファイルを指定するために、`app/views/layouts/application.html.erb`を修正します。`javascript_include_tag`の位置を`<head>`内から`</body>`の直前へ移動しています。

```udiff:app/views/layouts/application.html.erb
 <!DOCTYPE html>
 <html>
   <head>
     <title>HypernovaGettingStartedWithRails</title>
     <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
-    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
     <%= csrf_meta_tags %>
   </head>
   <body>
 
   <%= yield %>
 
+  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
   </body>
 </html>
```

### 9. 動作を確認してみる
Railsアプリを起動します。

```sh
% bin/rails server
```

起動したら、ブラウザで http://localhost:3000/welcome/index へアクセスし、`Hello, Hypernova!`と表示され、文字をクリックするとブラウザアラートボックスが表示されることが確認できるでしょう。

ページのソースを確認してみると、該当部分は以下のようになっていると思います。

```erb
<div data-hypernova-key="MyComponentjs"></div>
<script type="application/json" data-hypernova-key="MyComponentjs"><!--{"name":"Hypernova"}--></script>
```

これは、 **サーバサイドレンダリングを利用していない** のですが、正しくアプリは動作しています。

通常、ReactコンポーネントをDOMに描画する場合は、`ReactDOM.render`関数などを利用しますが、(7)の`Hypernova/react`の`renderReact`関数も同様の機能を備えていることが分かります。

#### サーバサイドレンダリング機能を有効にする
`Hypernova/server`サービスを起動すると、他のコードは変更しなくてもサーバサイドレンダリングが有効になります。サービスの起動は`node hypernova.js`をコマンドラインから実行します。

```sh
% node hypernova.js
2016-07-02T12:18:23.437Z - info: Connected
{ port: 3030 }
```

正常にサービスが起動すると上記のように`Connected`と表示されます。

この状態で再度Railsアプリ http://localhost:3000/welcome/index へアクセスすると、表示内容やクリック時の動作はサーバサイドレンダリングを利用しないときと同じですが、ページのソースを確認すると以下のように直接HTMLにコンポーネントが出力され **サーバサイドレンダリングを利用している** ことが分かります。

```erb
<div data-hypernova-key="MyComponentjs"><div data-reactroot="" data-reactid="1" data-react-checksum="-1229122185">Hello, Hypernova!</div></div>
<script type="application/json" data-hypernova-key="MyComponentjs"><!--{"name":"Hypernova"}--></script>
```

という感じで、簡単にサーバサイドレンダリングを利用できるようになりました。

## さいごに
本記事では、[airbnb/hypernova](https://github.com/airbnb/hypernova)を使ってRailsアプリでReactコンポーネントのサーバサイドレンダリングを実現するチュートリアルを説明しました。

`Hypernova`を使うと、通常じぶんで実装する必要のある「サーバ側で呼び出されたときのレンダリング処理」と「ブラウザ側で呼び出されたときのレンダリング処理」と「その分岐」をライブラリが肩代わりしてくれるため、意識せずともユニバーサルなReactコンポーネントを実装することができ大変楽ちんです。

ぜひ試してみてください。
もしお役に立ちそうなら「ストック」してもらえると嬉しいです。

次回は、`Hypernova`によるサーバサイドレンダリングを`Redux`コンテナに適用する方法を書く予定です。お楽しみに。

書きました：[RailsでReduxコンテナをサーバサイドレンダリング (use Hypernova by airbnb) - Qiita](http://qiita.com/noriaki/items/38fd9e9619c41ad74301)

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/airbnb?src=hash">#airbnb</a> 製のRailsにも対応しているJSサーバサイドレンダリング用ライブラリのHypernovaには <a href="https://twitter.com/hashtag/React?src=hash">#React</a> 用のバインディングはあるんだけど <a href="https://twitter.com/hashtag/Redux?src=hash">#Redux</a> (react-redux)用のものが無かったので書いた <a href="https://t.co/55eKymLGD4">https://t.co/55eKymLGD4</a></p>&mdash; 内山紀明(N.UCHIYAMA) (@noriaki) <a href="https://twitter.com/noriaki/status/748484384991248384">June 30, 2016</a></blockquote>


## 参考

- [hypernova/examples/simple at master · airbnb/hypernova](https://github.com/airbnb/hypernova/tree/master/examples/simple)