Posted at

Spring BootとGraalJSでVue.jsをSSRする試み


概要

GraalJSを触りがてら、SSRをやってみます。

JavaでもSSRしたいよって人の参考になれば幸いです。


GraalJSについて

GraalVMはJVMやJavaScript、Python、Rubyなどが動かせるVMです。

GraalJSはGraalVM環境で動作させることでパフォーマンスが最適化されるとのこと(公式より)ですが、Javaで書かれているため通常のJVMでも動作が可能となっています。

今回はJVMでGraalJSを使用しSSRを試みたいと思います。


環境


  • macOS Mojave

  • OpenJDK 11

  • Node.js v10.16.3

  • Yarn v1.17.3


JSファイル

JS側の設定等詳しく述べませんが、VueSSRガイドに沿って、Server側とClient側のJSファイルをwebpackで用意します。


webpack.config.js

    node: {

child_process: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
},

サーバー側で動かすために、上記を追加します。

Javaアプリの中で、vue-server-rendererとBundleファイルを読み込みHTMLを描画させます。


entry-server.js

import { createApp } from './app'

const { app, router, store } = createApp()

router.push(route)

router.onReady(() => {
renderVueComponentToString(app, (err, res) => {
rendered = String(res)
})
})


renderVueComponentToStringで取得されたHTMLをグローバル変数renderedに入れ込むように処理を書いています。


ScriptEngine

JavaのScriptEngineでGraalJSを使用するため、ライブラリを追加します。


pom.xml

        <!-- https://mvnrepository.com/artifact/org.graalvm.js/js -->

<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>19.2.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.graalvm.js/js-scriptengine -->
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>19.2.0</version>
</dependency>



Renderer.java

    public String render(String route) throws ScriptException, IOException {

ScriptEngine engine = getEngine();
ScriptContext context = getContext();
Bindings engineScope = engineSetting(engine, context);
engineScope.put("rendered", null); // グローバル変数宣言
engineScope.put("route", route); // グローバル変数宣言
engine.eval(read("static/js/server.js"), context);

return context.getAttribute("rendered").toString(); // rendered変数を取得しString型へ
}

private Bindings engineSetting(ScriptEngine engine, ScriptContext context) throws ScriptException, IOException {
context.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
Bindings engineScope = context.getBindings(ScriptContext.ENGINE_SCOPE);

engine.setContext(context);

engine.eval(
"var process = { env: { VUE_ENV: 'server', NODE_ENV: 'production' }}; this.global = { process: process };",
context);
loadFiles(engine, context);  // vue-server-rendererの読み込み

return engineScope;
}


Bindingsクラスのputメソッドでグローバル変数の宣言を行い、ScriptContextクラスのgetAttributeメソッドで値の取得ができます。


Renderer.java

    private ScriptEngine getEngine() {

return new ScriptEngineManager().getEngineByName("graal.js");
}

private ScriptContext getContext() {
return new SimpleScriptContext();
}


VueSSRガイドにあるようにJS側がシングルトンになることを避けるため、ScriptEngineはリクエストがあるたびに新しいインスタンスを作成するように処理しています。


描画

取得したHTMLをThymeleafで描画します。


index.html

<body th:utext="${rendered}">

</body>

Vue側では描画に使用するエレメントIDを含めて描画するようにしてください。


初期データ取得

初期描画時のデータはHTMLファイルにJSONを書き出すことで、取得します。

これはthymeleafで実現します。


index.html

    <script th:inline="javascript">

/*<![CDATA[*/
window.__INITIAL_STATE__ = /*[[${word}]]*/ {};
/*]]>*/
</script>

githubにサンプルプロジェクトをあげていますので、参考になれば幸いです。


感想

SSRはNuxtとかを使ってやったほうが簡単ですね。


参考

Vue.js サーバサイドレンダリングガイド

terwer/spring-vue-ssr