EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

JAVAとReact

弊社のサービスは、主にサーバーサイドはJAVAServlet/JSP、フロントエンドはJQueryで作られている所が多いのですが、色々と新しいアーキテクチャを使えるよう取り組んでいます。

サーバーサイドについては、プラットフォームをdockerコンテナにすることにより、spring-bootを使ったりrubyやGo、node.jsなど他言語でサービスを作ったりしています。 フロントエンドも、全く新しく作るページについてはVue.jsで実装してみたりしているのですが、サービス本体のページに対してもモダンなスタイルのJavaScriptを使えないかと検討しています。

JavaScriptフレームワークの課題

モダンJSを使うにあたり、ReactやVue.js、Angularなどのフレームワーク利用を考えますが、弊社のサービス的には以下の課題があります。

  1. SEOを強化したいページについて、静的HTMLとしてコンテンツを出力したい
  2. ファーストビューの表示が遅いと、ユーザーが離脱してしまう

1については、GoogleはJSの解釈も行うと言われていますが、メインコンテンツを動的に出力しようという所までその話を信頼できないと思っています。

今回は、これらの課題に対応しつつJSフレームワークを使うために、Servlet/JSPとJSによるサーバーサイドレンダリングを組み合わせる方法を模索してみました。

JAVAでReactを実行

今回はJSフレームワークとして、Reactで試してみようと思います。 Servlet/JSPとReactの組み合わせ方として、

案1 別プロセスでJavaScriptを実行するnode.jsを動かし、ServletからHTMLを出力する時にnode.jsでのSSR結果をincludeする 案2 JAVAのプロセス内でReactのSSRを実行し、その結果をJSP内に埋め込む

の2通りを考えたのですが、できたら面白そうだなということで、案2を試してみました。

JAVAにはNashornというJSスクリプトエンジンがあるので、それを使ってReactを動かしてみます。

Reactテストアプリ作成

まず、npmでcreate-react-appをインストールし、

npm install -g create-react-app

次にReactのテストアプリを作ります。

$ create-react-app react-test

Creating a new React app in /Xxxxx/xxxx/react-test.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts...

...

We suggest that you begin by typing:

  cd react-test
  yarn start

Happy hacking!

$ cd react-test
$ npm start

Starting the development server...

以上でブラウザが立ち上がり、Reactのサンプルページが出ます。

まず、ReactのサンプルページをSSRしてみます。

babelインストール

JAVAのNashornではJavaScriptは動かせますが、TypeScriptは解釈できません。ReactのサンプルページはTypeScriptで書かれているので、babelでTypeScriptからJavaScriptコンパイルします。

まずプロジェクトにbabel-cliとbabel-preset-reactをインストールします。

npm install --save-dev babel-cli babel-preset-react

次に、プロジェクトフォルダのpackage.jsonに以下を追加しました。

  "scripts": {
    "start": "react-scripts start",
+    "build": "babel src -d lib",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

TypeScriptのビルド

これで、以下でビルドできました。

$ npm run build

> react-test@0.1.0 build /Xxxxx/xxxx/react-test
> babel src -d lib

src/App.js -> lib/App.js
src/App.test.js -> lib/App.test.js
src/index.js -> lib/index.js
src/jvm-npm.js -> lib/jvm-npm.js
src/serviceWorker.js -> lib/serviceWorker.js

node.jsのrequire

Nashornにはnode.jsのrequire構文は無いので、jvm-npmを使いました。

jvm-npm - npm

JAVAの実装

以上の環境を作ってから、JAVA上でReactを実行するコードを以下のようにしました。

public class ReactTest {

    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByMimeType("application/javascript");

        // requireを使用できるように
        engine.eval("load('/Xxxxx/xxxx/react-test/src/jvm-npm.js');");

        // node.jsのグローバル変数対応
        engine.eval("var console = {warn: function() {}};");
        engine.eval("var process = {};");
        engine.eval("process.env = {};");
        engine.eval("process.env.NODE_ENV = 'development';");
        engine.eval("var global = {};");
        engine.eval("global.window = {};");

        // スクリプト読み込み
        BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("/Xxxxx/xxxx/react-test/lib/server.js")));
        StringBuilder sb = new StringBuilder();
        String line = reader.readLine();
        while (line != null) {
          sb.append(line).append('\n');
          line = reader.readLine();
        }

        String script = sb.toString();
        Object ret = engine.eval(script);
        System.out.println(ret);
    }
}

実行

SSRを実行するために、index.jsを書き換えserver.jsとして以下のコードを作りました。

import 'core-js/es6/map';
import 'core-js/es6/set';
import React from 'react';
import './lib/index.css';
import App from './lib/App';
import { renderToString } from 'react-dom/server'

renderToString(<App />);

上記コードを実行すると、以下のHTMLが取得できました。

<div class="App" data-reactroot=""><header class="App-header"><p>Edit <code>src/App.js</code> and save to reload.</p><a class="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a></header></div>

これをServletで実行しJSPに埋め込めば、JAVA上でのReactのSSRを実現できると思います。

ところが…

Reactのサンプルは単純なHTMLの生成だけだったので、別のRESTのAPIをaxiosで呼び出しページに出力するサンプルを作ろうとしたのですが、Nashornでaxiosを使えるようにすることができませんでした…。 axiosをimportすると、どうしてもload moduleのエラーが出てしまいます。

しかも、そもそもNashornはECMAScriptの仕様に追いつけないために非推奨となり、将来的に廃止となるようです。

JavaでJavaScriptを実行する「Nashorn」が非推奨に、ECMAScriptの速い進化に追いつけないと。代替案はGraalVM - Publickey

なので、NashornでのSSRは諦めて、GraalJSで試す(本気か!)か上記案1(確実)でSSRしてみようかなと思います。