Help us understand the problem. What is going on with this article?

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のサーバサイドレンダリングはできる。
  • サーバサイドレンダリングの単体性能も良さそう。高負荷時にどうなるかは今後試してみたい。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away