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

はじめに

以前書いた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のサーバサイドレンダリングはできる。
  • サーバサイドレンダリングの単体性能も良さそう。高負荷時にどうなるかは今後試してみたい。

『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。