Edited at

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

More than 1 year has passed since last update.


はじめに

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


作ったもの

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


ディレクトリ構成

前回とほぼ同じです。前回から変更した、ポイントとなる部分は※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-kitserver/react.goを参考にし、Gin向けに修正しています。

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


index.jsx

グローバル変数windowのあり/なしでサーバサイドで動作しているかどうかを判定し、処理を分けています。

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

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が流し込まれます。

<!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処理だけ抜粋してポイントを記載します。


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を使って読み込みますが、bindataを使ってバイナリファイルからこのテンプレートファイルを読ませるためにmultitemplateを使っています。詳しくは、Use templates from go-bindata?を参照してください。


  • 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が生成されます。

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__はコールバック関数が設定されている変数名です。

// 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構造体のデータを生成し、チャネルを通じて返す関数が実装されています。

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構造体の定義は以下のとおり。Appindex.jsxで生成したHTML文字列が設定されます。その文字列は、HTMLApp関数を経由してreact.htmlテンプレートに埋め込まれています。

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が埋め込まれていることが分かります。

<!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.gobundle.jsをプールさせずに動かしていたためでした。main.goNewReactの引数にfalseを渡してデバッグモードをオフにして再実行してみます。


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のサーバサイドレンダリングはできる。

  • サーバサイドレンダリングの単体性能も良さそう。高負荷時にどうなるかは今後試してみたい。