Golang GinでReact.jsのサーバサイドレンダリングを試してみた

# はじめに

以前書いた[GolangのGin/bindataでシングルバイナリを試してみた(+React)](http://qiita.com/wadahiro/items/4173788d54f028936723)の続きです。今回はReactのサーバサイドレンダリングを追加で試してみました。なお、Golangで使えるJavaScriptエンジンはいくつかありますが、今回は[go-duktape](https://github.com/olebedev/go-duktape)を使っています。サイトにベンチマークが記載されておりそこそこ速そうです。また、`go-duktape`の作者の方が[go-starter-kit](https://github.com/olebedev/go-starter-kit)というEcho + Reactのサーバサイドレンダリングのサンプルを作ってくれているため、こちらを参考にしてGin向けに作ってみました。

# 作ったもの

https://github.com/wadahiro/gin-react-boilerplate/tree/server-side-rendering にあります。

# ディレクトリ構成

[前回](http://qiita.com/wadahiro/items/4173788d54f028936723)とほぼ同じです。前回から変更した、ポイントとなる部分は※1～4のファイルです。

```
gin-react-boilerplate
├─assets
│  ├─css
│  │  └─main.css
│  ├─index.html
│  └─js
│      └─bundle.js
├─client
│  ├─app
│  │  └─App.jsx
│  ├─index.jsx  ※1
│  └─webpack
│      ├─webpack.base.config.js
│      ├─webpack.config.js
│      ├─webpack.dev.config.js
│      ├─webpack.prod.config.js
│      └─webpack.server.js
├─dist
├─.gitignore
├─package.json
├─runner.conf
└─server
   ├─controllers
   │  └─home.go
   ├─templates
   │  └─react.html  ※2
   │─main.go  ※3
   │─react.go  ※4
   └─utils.go
```

* ※1 `index.jsx`: Reactのサーバーサイドレンダリング用のコードを追加します。
* ※2 `react.html`: サーバサイドのHTMLテンプレート処理で使用します。
* ※3 `main.go`: GinでサーバサイドでHTMLを生成して返すようにハンドラを設定します。
* ※4 `react.go`: `go-duktape`を使ってJavaScriptをサーバサイドで処理させる部分。GinのHandleを実装しています。 `go-duktape`の作者が作っている[go-starter-kit](https://github.com/olebedev/go-starter-kit)の`server/react.go`を参考にし、Gin向けに修正しています。

以下、上記4ファイルについて詳細を記載していきます。

# `index.jsx`

グローバル変数`window`のあり/なしでサーバサイドで動作しているかどうかを判定し、処理を分けています。
サーバサイドの場合は`global.main`関数を設定しています。この関数が`react.go`の処理から呼び出されます。関数内では、サーバサイドレンダリング用のAPIである`ReactDOMServer.renderToString`を使ってHTML文字列を生成します。`callback`関数に生成したHTML文字列を渡していますが、この`callback`関数も`react.go`の中で実装されている関数になり、そちらで最終的なHTMLレスポンスを返しています。

```js
if (typeof window !== 'undefined') {
    ReactDOM.render(<App />, document.getElementById('app'));
} else {
    global.main = (options, callback) => {
        // console.log('render server side', JSON.stringify(options))
        const s = ReactDOMServer.renderToString(React.createElement(App, {}));

        callback(JSON.stringify({
            uuid: options.uuid,
            app: s,
            title: null,
            meta: null,
            initial: null,
            error: null,
            redirect: null
        }));
    };
}
```

# `react.html`

サーバサイドレンダリング時に返すHTMLのテンプレートに使います。ポイントは`<div id="app">{{ .HTMLApp }}</div>`の部分。ここに先ほどの`ReactDOMServer.renderToString`で生成されたHTMLが流し込まれます。

```html
<!DOCTYPE html>
<html data-uuid="{{ .UUID }}">
  ...
  <body>
    ...
    <div id="app">{{ .HTMLApp }}</div>
    <script onload="this.parentElement.removeChild(this)">window['--app-initial'] = JSON.parse("{{if .Initial}}{{ .Initial }}{{else}}{}{{end}}");</script>
    <script async defer src="js/bundle.js" onload="this.parentElement.removeChild(this)"></script>
  </body>
</html>
```

# `main.go`

main処理だけ抜粋してポイントを記載します。

```go

func main() {
	r := gin.Default()

	r.HTMLRender = loadTemplates("react.html")

	r.Use(func(c *gin.Context) {
		id, _ := uuid.NewV4()
		c.Set("uuid", id)
	})

	// add routes
	r.GET("/api/home", controllers.Home)

	react := NewReact(
		"assets/js/bundle.js",
		true,
		r,
	)

	r.GET("/", react.Handle)

	// We can't use router.Static method to use '/' for static files.
	// see https://github.com/gin-gonic/gin/issues/75
	// r.StaticFS("/", assetFS())
	r.Use(static.Serve("/", BinaryFileSystem("assets")))

	port := os.Getenv("PORT")
	if len(port) == 0 {
		port = "3000"
	}
	r.Run(":" + port)
}
```

* `r.HTMLRender = loadTemplates("react.html")` でテンプレートファイルを読み込んでいます。通常、Ginだと[LoadHTMLGlob](https://github.com/gin-gonic/gin#html-rendering)を使って読み込みますが、bindataを使ってバイナリファイルからこのテンプレートファイルを読ませるために[multitemplate](https://github.com/gin-gonic/contrib/tree/master/renders/multitemplate)を使っています。詳しくは、[Use templates from go-bindata?](https://github.com/gin-gonic/contrib/issues/23)を参照してください。
* `react.go`で定義している`NewReact`を呼びだし、`r.GET("/", react.Handle)`でGinのハンドラとして設定します。これで`/`以下のアクセスでサーバサイドレンダリングの結果が返されます。なお、`NewReact`の第2引数に`true`を渡していますが、`false`を渡すと`bundle.js`を最初に読み込んでプールするようになり、高速に動作するようになります。開発時はJavaScriptの変更をサーバサイドレンダリングに反映させたいため、`false`を渡します。

# `react.go`

長いのでポイントだけ抜粋。

* Handle関数がGinのハンドラ実装。ここがリクエスト受けつけた時の起点となります。`vm.Handle`関数の処理結果としてレスポンス用の構造化データ`Resp`を受け取り、`c.HTML(http.StatusOK, "react.html", re)`にてHTMLテンプレートに流し込まれ、最終的なレスポンスとなるHTMLが生成されます。

```go
func (r *React) Handle(c *gin.Context) {
	...

	select {
	case re := <-vm.Handle(map[string]interface{}{
		"url":     c.Request.URL.String(),
		"headers": c.Request.Header,
		"uuid":    UUID.String(),
	}):
		re.RenderTime = time.Since(start)
		// Return vm back to the pool
		r.put(vm)
		// Handle the Response
		if len(re.Redirect) == 0 && len(re.Error) == 0 {
			// If no redirection and no errors
			c.Header("X-React-Render-Time", fmt.Sprintf("%s", re.RenderTime))
			c.HTML(http.StatusOK, "react.html", re)
			// If redirect
		} else if len(re.Redirect) != 0 {
			c.Redirect(http.StatusMovedPermanently, re.Redirect)
			// If internal error
		} else if len(re.Error) != 0 {
			c.Header("X-React-Render-Time", fmt.Sprintf("%s", re.RenderTime))
			c.HTML(http.StatusInternalServerError, "react.html", re)
		}
```

* `vm.Handle`関数の中身です。`PevalString`関数を呼び、`index.jsx`で定義されていたJavaScriptの`global.main`関数を実行しています。`__goServerCallback__`はコールバック関数が設定されている変数名です。

```go
// Handle handles http requests
func (r *ReactVM) Handle(req map[string]interface{}) <-chan Resp {
	b, err := json.Marshal(req)
	Must(err)
	// Keep it sync with `client/index.jsx`
	r.PevalString(`main(` + string(b) + `, __goServerCallback__)`)
	return r.ch
}
```

* `__goServerCallback__`は`newReactVM`関数の中で初期化処理時に定義されています。`index.jsx`から受け取ったJSON文字列からResp構造体のデータを生成し、チャネルを通じて返す関数が実装されています。

```go
func newReactVM(filePath string, engine http.Handler) *ReactVM {
	...

	vm.PushGlobalGoFunction("__goServerCallback__", func(ctx *duktape.Context) int {
		result := ctx.SafeToString(-1)
		vm.ch <- func() Resp {
			var re Resp
			json.Unmarshal([]byte(result), &re)
			return re
		}()
		return 0
	})
```

* `Resp`構造体の定義は以下のとおり。`App`に`index.jsx`で生成したHTML文字列が設定されます。その文字列は、`HTMLApp`関数を経由して`react.html`テンプレートに埋め込まれています。

```go
type Resp struct {
	UUID       string        `json:"uuid"`
	Error      string        `json:"error"`
	Redirect   string        `json:"redirect"`
	App        string        `json:"app"`
	Title      string        `json:"title"`
	Meta       string        `json:"meta"`
	Initial    string        `json:"initial"`
	RenderTime time.Duration `json:"-"`
}

// HTMLApp returns a application template
func (r Resp) HTMLApp() template.HTML {
	return template.HTML(r.App)
}

// HTMLMeta returns a meta data
func (r Resp) HTMLMeta() template.HTML {
	return template.HTML(r.Meta)
}
```

# 動作確認

ビルド＆実行して http://localhost:3000/ にアクセスすると下記のHTMLが返ります。Reactのサーバサイドレンダリングをしない場合は`<div id="app"></div>`となりますが、ちゃんとサーバサイドでReactのレンダリングが行われた結果のHTMLが埋め込まれていることが分かります。

```html
<!DOCTYPE html>
<html data-uuid="7dca6e5c-3b3d-40fb-778d-867fc2a05425">
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="css/main.css">
    <title></title>
    
  </head>
  <body>
    
    <div id="app"><div data-reactid=".b363pa5tp4" data-react-checksum="133442110"><span data-reactid=".b363pa5tp4.0">Message: </span><span data-reactid=".b363pa5tp4.1"></span></div></div>
    <script onload="this.parentElement.removeChild(this)">window['--app-initial'] = JSON.parse("{}");</script>
    <script async defer src="js/bundle.js" onload="this.parentElement.removeChild(this)"></script>
  </body>
</html>
```

前回同様、シングルバイナリとしてももちろん動作します。

# 性能
サーバサイドレンダリングの処理時間はどんなものか、単体性能だけ見てみました。
Ginが出す処理時間を確認すると、`192ms`と微妙な結果が、、、？

```
[GIN] 2016/03/30 - 12:31:18 |[97;42m 200 [0m|    192.0244ms | ::1 |[97;44m  [0m GET     /
[GIN] 2016/03/30 - 12:31:18 |[90;47m 304 [0m|             0 | ::1 |[97;44m  [0m GET     /css/main.css
[GIN] 2016/03/30 - 12:31:18 |[90;47m 304 [0m|     20.0026ms | ::1 |[97;44m  [0m GET     /js/bundle.js
[GIN] 2016/03/30 - 12:31:19 |[97;42m 200 [0m|             0 | ::1 |[97;44m  [0m GET     /api/home
```

これは単に`react.go`で`bundle.js`をプールさせずに動かしていたためでした。`main.go`の`NewReact`の引数に`false`を渡してデバッグモードをオフにして再実行してみます。

```go:main.go
	react := NewReact(
		"assets/js/bundle.js",
		false,
		r,
	)
```

結果はプールが効き、`2.5ms`とサーバサイドレンダリングの単体性能はかなり速い！。

```
[GIN] 2016/03/30 - 12:32:44 |[97;42m 200 [0m|      2.5003ms | ::1 |[97;44m  [0m GET     /
[GIN] 2016/03/30 - 12:32:44 |[90;47m 304 [0m|             0 | ::1 |[97;44m  [0m GET     /css/main.css
[GIN] 2016/03/30 - 12:32:45 |[90;47m 304 [0m|     21.5027ms | ::1 |[97;44m  [0m GET     /js/bundle.js
[GIN] 2016/03/30 - 12:32:45 |[97;42m 200 [0m|             0 | ::1 |[97;44m  [0m GET     /api/home
```


# まとめ
* GolangでもReactのサーバサイドレンダリングはできる。
* サーバサイドレンダリングの単体性能も良さそう。高負荷時にどうなるかは今後試してみたい。