gRPC-Web と Server Side Rendering の苦労話

Adventarを支える技術 Advent Calendar 2019 の5日目です。

今日は gRPC-Web 導入にあたって最も苦労した Server Side Rendering(以降は SSR と書きます)の話を書きます。

前提条件

初日の記事 に書いたとおり、今回の Adventar のシステム変更の目的に、

  • gRPC-Web を使う
  • Serverless な仕組みで SSR をおこなう

というのを入れました。趣味プロダクトなので技術が目的になっています。なので、gRPC-Web をやめる、SSR をしない、という選択肢はそもそも取らない、という前提で読んでください。(どちらかをやめることができればどれほど楽だったか...)

また、ここでいう SSR とは Nuxt.js などで作った JavaScript のアプリケーションを同一コードでサーバー側でも動かして HTML をレンダリングする、という文脈のやつです。Universal JavaScript と言い換えてもよいです。

個人的には SPA の SSR は実装難易度や運用コストが増えるので、やらないで済むならやらないほうがいい派なんですが、今回は普段やらないようなこともやってみるというのも目的の一つにありました。

gRPC-Web が Node.js で利用できない問題

gRPC-Web の自動生成したクライアントは完全にブラウザ用になっていて、Node.js では利用できません。何が困るかというと、SSR する際にブラウザとサーバーで同一のコードが使えないので詰んでしまいます。

ブラウザ用の Polyfill などを入れてみたりしましたが、どうしても動かないので諦めました。正攻法は gRPC-Web のクライアント生成に手を入れて Node.js 用のトランスポートオプションをサポートすることだと思いますが、C++ (protoc の gRPC-Web プラグインの実装言語)が苦手すぎて今回は諦めました。

そこで今回は妥協案として、Node.js 向けに JSON API を提供することにしました。JSON API というのは普通に ContentType: application/json で通信する API という意味です。つまりブラウザと API サーバー間は gRPC-Web で通信しますが、SSR する場合の Node.js と API サーバー間は JSON API で通信します。最悪な妥協策というのは理解しています。

envoy を使った JSON API Proxy

API を2種類用意するといっても、サーバーを別途を立てたわけではありません。gRPC-Web の proxy として利用している envoy に、JSON API を gRPC に変換する機能があるため、gRPC-Web と JSON API を同一のプロセスで処理できます。

f:id:hokaccha:20191203213418p:plain

envoy のこの機能については後日詳しく紹介しますが、少し設定を足すだけで済むので、サーバー側は特に手間はかかりません。2つの API があることで面倒なのはクライアント側だけということです。

クライアント側のコード

クライアント側の対応はサーバー側よりは面倒ですが、そこまで複雑ではありません。以下のように gRPC のクライアントと JSON API のクライアントを用意します。

そして以下のように ブラウザが実行する場合は gRPC、SSR の場合は JSON API を呼び出すだけです。

SSR は基本的に更新系のAPIを呼び出すことはないですし、今回は SSR するのをカレンダーの詳細ページ( https://adventar.org/calendars/3860 のようなページ)だけに留めたので、最終的にはこれぐらいのコード量でなんとかなりました。しかしここに行き着くまでにはだいぶ苦労しました...。

まとめ

仕事だったらおそらく頑張って Node.js のトランスポートオプションを実装するか、gRPC-Web を諦めて JSON API に寄せるか、SSR を諦めて別の方法を取る道をさぐったかもしれませんが、今回は前提条件にもあるように、gRPW-Web と SSR は妥協できなかったのでこのような方法を採用しました。来年時間が取れれば gRPC-Web に Node.js サポートパッチを書きたいです。

明日は envoy の便利機能について書く予定です。