Vue.js 2.0+Spring Bootでサーバーサイドレンダリングを実現するサンプルを書いた

Vue.js 2.0+Spring Bootでサーバーサイドレンダリングを実現するサンプルを書いた

github.com

Vue.js 2.0がサーバーサイドレンダリングに対応した話は Vue.js 2.0のServer Side Renderingを試しつつ、軽くコードリーディングした - kitak blog に書いた通りで、その記事ではNode.jsでレンダリングをおこなった。ただ、個人的な理由で、Javaで出来るかどうか検証したかったのでGitHub - winterbe/spring-react-example: Isomorphic Spring Boot React.js ExampleをベースにReactをVue.jsに置き換える形で検証した。

サーバーサイドレンダリングの必要性

そもそも、なぜサーバーサイドレンダリングが必要か。Vue.js 2.0のServer Side Renderingを試しつつ、軽くコードリーディングした - kitak blog の記事では、利用者や検索エンジンでのクローラー視点で必要性を書いたが、加えて開発者の視点では、クライアントサイドのJavaScriptで扱うテンプレートとサーバーサイドのテンプレートを共通化できるというメリットがある。サーバーサイドとクライアントサイドでテンプレートが混在すると何が問題かというと、テンプレートの境界で脆弱性が発生したり( refs: AngularJSとサーバーサイドテンプレートの混在とngNonBindable ::ハブろぐ )、デリミタ({{ }})がクライアントサイドとサーバーサイドのテンプレートエンジンでダブってエラーになったり、無限スクロールを実装するときにサーバーサイドとクライアントサイドで同じ内容のテンプレートを記述することになり、メンテナンスが大変になったりする。

次からサンプルで重要そうな端々を紹介する。

Vue.js 2.0 の renderToString

Vue.js周辺のコードに関しては、先日書いた記事( Vue.js 2.0のServer Side Renderingを試しつつ、軽くコードリーディングした - kitak blog )の内容とほとんど同じなので説明を省く。テンプレートをVirtual DOMを返す関数に変換して、renderToStringを呼んでいるだけ。

spring-vue-example/components.js at 5c1f4734b0b0a0d4bdf21df3365ed2c560231da2 · kitak/spring-vue-example · GitHub

Nashornを利用したレンダリング

NashornはJava8から搭載されているJavaScriptエンジン。以下のようにJavaScriptのコードをロードして、JavaScriptで定義された関数を呼び出すことができる。

spring-vue-example/VueRenderer.java at 5c1f4734b0b0a0d4bdf21df3365ed2c560231da2 · kitak/spring-vue-example · GitHub

NashornScriptEngine nashornScriptEngine = (NashornScriptEngine) new ScriptEngineManager().getEngineByName("nashorn"); nashornScriptEngine.eval(read("static/event-loop.js"));
nashornScriptEngine.eval(read("static/nashorn-polyfill.js"));
nashornScriptEngine.eval(read("static/server.js"));

// ...

Object html = nashornScriptEngine.invokeFunction("renderServer", comments);

読み込んでいるJavaScriptのevent-loop.jsやnashorn-polyfill.jsは、Vue.js 2.0のServer rendererがNode.js(v8)での実行をある程度想定しているので、以下の様な足りないconsoleやprocess、setTimeoutのpolyfillを用意する必要があるため、その定義。ここらへんちょっと微妙...

spring-vue-example/event-loop.js at 5c1f4734b0b0a0d4bdf21df3365ed2c560231da2 · kitak/spring-vue-example · GitHub
spring-vue-example/nashorn-polyfill.js at 5c1f4734b0b0a0d4bdf21df3365ed2c560231da2 · kitak/spring-vue-example · GitHub

var console = {};
console.debug = print;
console.error = print;
console.warn = print;
console.log = print;

var process = {};
process.env = {};
process.nextTick = function(fn) {
  global.setTimeout(fn, 0);
};

サンプルのmasterブランチでは、NashornScriptEngineのインスタンスの管理、HTMLの生成を担うクラス VueRenderer を定義して、そのインスタンスが生成したHTMLをテンプレートに埋め込んでいる。最近のSpringだと、Script templatesという機能でNashorn, JRuby, Jythonから、各言語のテンプレートエンジンを利用することができるので、それを使ったサンプルもset-view-resolverブランチに用意した。

サーバーを起動して、アクセスしたところ、ページの表示やサーバー側とクライアント側のデータの同期処理( hydrationと呼ばれている )も意図通りおこなわれていることを確認した。この同期処理がうまくいかないと、サーバー側でレンダリングした内容がクライアント側のVue.jsが生成したDOM要素にまるっと置き換えられてしまって、サーバ側でレンダリングした意味がなくなる。

と、一見うまくいってそうに思えたのだけど、以下の問題がある。

既知の問題

spring-react-exampleでは、リクエスト毎にNashornScriptEngineのインスタンスを生成・スクリプトをロードしているが、自分が書いたサンプルでは、Browserifyで生成したファイルの読み込みに数秒かかってしまっているので(そもそもなんでそんなにかかっているのか、という気もするが... 時間があるときに調べる)、起動時に一度ロードしたものを再利用している。コードを読んだ限り、renderToString関数の呼び出し間で値の共有は無いので、並列に実行されることで、競合は起きないように思えるが、正直Javaに疎いので自信がない。一度読み込んでしまった後であれば、実行は体感的に早いように感じる。

どの程度パフォーマンスが出るか、abで負荷をかけてみたが、サーバー起動から17回目のリクエストでViewModelのオブジェクトを想定しているthisがundefinedになってしまった。これが面白くて何度起動し直してもぴったり17回目で問題が起きる。あれー...と思って、自分の書いたプログラムかVue.jsのどちらかの問題かなぁ、と疑ってかかっていたが、根気よく調べたら、Nashornのバグであることが発覚した。


報告して、バグとして登録してもらった。Bug ID: JDK-8160034 The `this` value in the `with` is broken by the repetition of a function call

実現はできているが、上のNashornのバグもあるし、Vue.js 2.0もまだalphaなので、実際にproductionに採用するには、まだまだというところ。仮にNashornのバグが修正されて(あるいはNashornのバグを回避するために、Virtual DOMを返す関数を自分で書くという手もあるが、うーん...)、Vue.js 2.0の正式バージョンがリリースされたとしても、Node.js( V8 )をある程度前提にして書かれたコードをNashornで動かすことや、Nashornをレンダリングに利用して安定的に運用する、という課題を解消するためにそれなりにコストがかかりそう。そのコストを払ってでも、先に挙げたサーバーサイドレンダリングで得られるものが重要なのであれば、おこなう価値はあるのではなかろうか。
しばらくはVue.js 2.0 開発版のリリースに追従しつつ、寝かせるつもり。