はじめに
以前書いた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-kitのserver/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
構造体の定義は以下のとおり。App
にindex.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.go
でbundle.js
をプールさせずに動かしていたためでした。main.go
のNewReact
の引数にfalse
を渡してデバッグモードをオフにして再実行してみます。
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のサーバサイドレンダリングはできる。
- サーバサイドレンダリングの単体性能も良さそう。高負荷時にどうなるかは今後試してみたい。