
Reactで同型アプリケーションを作成するには、通常、Node.jsをサーバー側として使用します。 ただし、サーバーがJavaで作成されている場合は、同形アプリケーションを放棄しないでください。Javaには、Reactを使用したHTMLのサーバー側レンダリングに対応できる組み込みJavaScriptエンジン(Nashorn)が含まれています。
Javaサーバーを使用したサーバー側のReactレンダリングを示すアプリケーションのコードはGitHubにあります 。 記事で私は考慮します:
- NettyおよびJAX-RS ( Reseasyの実装)に基づいたマイクロサービススタイルのJavaサーバー。Web要求を処理し、 Dockerで実行する機能を備えています。
- CDIライブラリーを使用した依存性注入( Weld SE実装)。
- Webpack 2を使用してJavaScriptバンドルをビルドします。
- Reactを使用してサーバーでHTMLレンダリングを設定します。
- Webpack dev serverを使用して、ページとスタイルのホットリロードをサポートしたデバッグを実行します 。
Javaサーバー
マイクロサービス(サーブレットコンテナを使用する必要のない自己実行jar)のスタイルで、Javaでサーバーを作成することを検討してください。 依存関係管理のライブラリとして、CDI標準(Contexts and Dependency Injection)を使用します。これはJava EEの世界から来ましたが、Java SEアプリケーションで使用できます。 CDI実装-Weld SEは、依存関係管理のための強力で十分に文書化されたライブラリです。 CDIには、他のライブラリへの多くのバインダーがあります。たとえば、アプリケーションはJAX-RSおよびNetty用のCDIバインダーを使用します。 src / main / resources / META-INFディレクトリ(このモジュールがCDIをサポートするという宣言)にbeans.xmlファイルを作成し、クラスを標準属性でマークアップし、コンテナを初期化するだけで十分です。依存関係を注入できます。 特別な注釈が付けられたクラスは自動的に登録されます(手動登録も可能です)。
// . public static void main(String[] args) { // JUL SLF4J. SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); LOG.info("Start application"); // CDI http://weld.cdi-spec.org/ final Weld weld = new Weld(); // . weld.property(Weld.SHUTDOWN_HOOK_SYSTEM_PROPERTY, false); final WeldContainer container = weld.initialize(); // Netty JAX-RS, CDI . final CdiNettyJaxrsServer nettyServer = new CdiNettyJaxrsServer(); ............... // web . nettyServer.start(); .............. // TERM . try { final CountDownLatch shutdownSignal = new CountDownLatch(1); Runtime.getRuntime().addShutdownHook(new Thread(() -> { shutdownSignal.countDown(); })); try { shutdownSignal.await(); } catch (InterruptedException e) { } } finally { // CDI . nettyServer.stop(); container.shutdown(); LOG.info("Application shutdown"); SLF4JBridgeHandler.uninstall(); } } // , "" @ApplicationScoped public class IncrementService { .............. } // @NoCache @Path("/") @RequestScoped @Produces(MediaType.TEXT_HTML + ";charset=utf-8") public class RootResource { /** * {@link IncrementService}. */ @Inject private IncrementService incrementService; .............. }
CDI依存関係を持つクラスをテストするには、JUnitのArquillian拡張機能を使用します。
単体テスト
/** * {@link IncrementResource}. */ @RunWith(Arquillian.class) public class IncrementResourceTest { @Inject private IncrementResource incrementResource; /** * @return , CDI. */ @Deployment public static JavaArchive createDeployment() { return ShrinkWrap.create(JavaArchive.class) .addClass(IncrementResource.class) .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); } @Test public void getATest() { final Map<String, Integer> response = incrementResource.getA(); assertNotNull(response.get("value")); assertEquals(Integer.valueOf(1), response.get("value")); } .............. /** * {@link IncrementService}. RequestScoped: * Arquillian . * @return {@link IncrementService}. */ @Produces @RequestScoped public IncrementService getIncrementService() { final IncrementService service = mock(IncrementService.class); when(service.getA()).thenReturn(1); when(service.incrementA()).thenReturn(2); when(service.getB()).thenReturn(2); when(service.incrementB()).thenReturn(3); return service; } }
組み込みのウェブサーバーNettyを介してウェブリクエストの処理を設定します。 関数-ハンドラーを作成するために、Java EE、JAX-RSに由来する別の標準を使用します。 JAX-RS標準の実装として、Resteasyライブラリを選択します。 Netty、CDI、およびResteasyを接続するには、 resteasy-netty4-cdiモジュールが使用されます。 JAX-RSは、javax.ws.rs.core.Application下位クラスを使用して構成されます。 通常、要求ハンドラーおよびその他のJAX-RSコンポーネントはそこに登録されます。 CDIとResteasyを使用する場合、JIX-RSに登録されたリクエストハンドラー(JAX-RS:Pathアノテーションでマーク)およびプロバイダーと呼ばれる他のJAX-RSコンポーネント(JAX-RS:プロバイダーアノテーションでマーク)がJAX-RSコンポーネントとして使用されることを示すだけで十分です。 。 ドキュメントからResteasyの詳細を学ぶことができます 。
NettyおよびJAX-RSアプリケーション
public static void main(String[] args) { ............... // Netty JAX-RS, CDI . // JAX-RS Resteasy http://resteasy.jboss.org/ final CdiNettyJaxrsServer nettyServer = new CdiNettyJaxrsServer(); // Netty ( ). final String host = configuration.getString( AppConfiguration.WEBSERVER_HOST, AppConfiguration.WEBSERVER_HOST_DEFAULT); nettyServer.setHostname(host); final int port = configuration.getInt( AppConfiguration.WEBSERVER_PORT, AppConfiguration.WEBSERVER_PORT_DEFAULT); nettyServer.setPort(port); // JAX-RS. final ResteasyDeployment deployment = nettyServer.getDeployment(); // JAX-RS ( ). deployment.setInjectorFactoryClass(CdiInjectorFactory.class.getName()); // , JAX-RS . deployment.setApplicationClass(ReactReduxIsomorphicExampleApplication.class.getName()); // web . nettyServer.start(); ............... } /** * JAX-RS */ @ApplicationScoped @ApplicationPath("/") public class ReactReduxIsomorphicExampleApplication extends Application { /** * CDI Resteasy. */ @Inject private ResteasyCdiExtension extension; /** * @return JAX-RS. */ @Override @SuppressWarnings("unchecked") public Set<Class<?>> getClasses() { final Set<Class<?>> result = new HashSet<>(); // CDI Resteasy JAX-RS. result.addAll((Collection<? extends Class<?>>) (Object)extension.getResources()); // CDI Resteasy JAX-RS. result.addAll((Collection<? extends Class<?>>) (Object)extension.getProviders()); return result; } }
すべての静的ファイル(javascriptバンドル、css、写真)はクラスパス(src / main / resources / webapp)に配置され、結果のjarファイルに収まります。 このようなファイルにアクセスするには、{fileName :. *}。{Ext}という形式のURLハンドラーが使用され、クラスパスからファイルがロードされ、クライアントに渡されます。
静的リクエストハンドラ
/** * . * <p> {filename}.{ext}</p> */ @Path("/") @RequestScoped public class StaticFilesResource { private final static Date START_DATE = DateUtils.setMilliseconds(new Date(), 0); @Inject private Configuration configuration; /** * . classpath. * @param fileName . * @param ext . * @param uriInfo URL , . * @param request . * @return 404 - . * @throws Exception . */ @GET @Path("{fileName:.*}.{ext}") public Response getAsset( @PathParam("fileName") String fileName, @PathParam("ext") String ext, @Context UriInfo uriInfo, @Context Request request) throws Exception { if(StringUtils.contains(fileName, "nomin") || StringUtils.contains(fileName, "server")) { // . return Response.status(Response.Status.NOT_FOUND) .build(); } // ifModifiedSince . classpath, // . final ResponseBuilder builder = request.evaluatePreconditions(START_DATE); if (builder != null) { // . return builder.build(); } // classpath. final String fileFullName = "webapp/static/" + fileName + "." + ext; // . final InputStream resourceStream = ResourceUtilities.getResourceStream(fileFullName); if(resourceStream != null) { // , . final String cacheControl = configuration.getString( AppConfiguration.WEBSERVER_HOST, AppConfiguration.WEBSERVER_HOST_DEFAULT); // . return Response.ok(resourceStream) .type(URLConnection.guessContentTypeFromName(fileFullName)) .cacheControl(CacheControl.valueOf(cacheControl)) .lastModified(START_DATE) .build(); } // . return Response.status(Response.Status.NOT_FOUND) .build(); } }
React HTMLサーバーレンダリング
Javaアプリケーションを構築するときにバンドルを構築するには、maven frontend-maven-pluginプラグインを使用できます 。 希望するバージョンのNodeJを個別にダウンロードしてローカルに保存し、webpackを使用してバンドルを構築します。 mvnコマンド(またはmavenとの統合をサポートするIDE)でJavaプロジェクトの通常の構築を開始するだけで十分です。 クライアントのjavascript、styles、package.json、webpack構成ファイルをsrc / main / frontendディレクトリに配置し、結果のバンドルをsrc / main / resources / webapp / static / assetに配置します。
fronend-maven-pluginのセットアップ
<plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <configuration> <nodeVersion>v${node.version}</nodeVersion> <npmVersion>${npm.version}</npmVersion> <installDirectory>${basedir}/src/main/frontend</installDirectory> <workingDirectory>${basedir}/src/main/frontend</workingDirectory> </configuration> <executions> <!-- nodejs npm . --> <execution> <id>nodeInstall</id> <goals> <goal>install-node-and-npm</goal> </goals> </execution> <!-- npm src/main/frontend/package.json. --> <execution> <id>npmInstall</id> <goals> <goal>npm</goal> </goals> </execution> <!-- webpack. --> <execution> <id>webpackBuild</id> <goals> <goal>webpack</goal> </goals> <configuration> <skip>${webpack.skip}</skip> <arguments>${webpack.arguments}</arguments> <srcdir>${basedir}/src/main/frontend/app</srcdir> <outputdir>${basedir}/src/main/resources/webapp/static/assets</outputdir> <triggerfiles> <triggerfile>${basedir}/src/main/frontend/webpack.config.js</triggerfile> <triggerfile>${basedir}/src/main/frontend/package.json</triggerfile> </triggerfiles> </configuration> </execution> </executions> </plugin>
JAX-RSで独自のHTMLページジェネレーターを設定するには、クラスを作成し、javax.ws.rs.ext.MessageBodyWriterインターフェースを実装するプロバイダーアノテーションでハンドラーを作成し、Web要求ハンドラーから応答として返す必要があります。
サーバーのレンダリングは、組み込みのJava JavaScriptエンジンであるNashornを使用して実行されます。 これはシングルスレッドスクリプトエンジンです。複数の同時リクエストを処理するには、エンジンの複数のキャッシュインスタンスを使用する必要があります。リクエストごとに無料のインスタンスが取得され、HTMLがレンダリングされ、プール( Apache Commons Pool 2 )に返されます。
/** * web-. */ public class ViewResult { private final String template; private final Map<String, Object> viewData = new HashMap<>(); private final Map<String, Object> reduxInitialState = new HashMap<>(); .............. } /** * , {@link ViewResult} HTML. * <p> * React HTML (React Isomorphic), * , React. * </p> */ @Provider @ApplicationScoped public class ViewResultBodyWriter implements MessageBodyWriter<ViewResult> { .............. private ObjectPool<AbstractScriptEngine> enginePool = null; @PostConstruct public void initialize() { // . final boolean useIsomorphicRender = configuration.getBoolean( AppConfiguration.WEBSERVER_ISOMORPHIC, AppConfiguration.WEBSERVER_ISOMORPHIC_DEFAULT); final int minIdleScriptEngines = configuration.getInt( AppConfiguration.WEBSERVER_MIN_IDLE_SCRIPT_ENGINES, AppConfiguration.WEBSERVER_MIN_IDLE_SCRIPT_ENGINES_DEFAULT); LOG.info("Isomorphic render: {}", useIsomorphicRender); if(useIsomorphicRender) { // React , // javascript . Javascript , // javascript. final GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMinIdle(minIdleScriptEngines); enginePool = new GenericObjectPool<AbstractScriptEngine>(new ScriptEngineFactory(), config); } } @PreDestroy public void destroy() { if(enginePool != null) { enginePool.close(); } } .............. @Override public void writeTo( ViewResult t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { .............. if(enginePool != null && t.getUseIsomorphic()) { // React . try { // javascript. final AbstractScriptEngine scriptEngine = enginePool.borrowObject(); try { // URL , react-router . final String uri = uriInfo.getPath() + (uriInfo.getRequestUri().getQuery() != null ? (String) ("?" + uriInfo.getRequestUri().getQuery()) : StringUtils.EMPTY); // React. final String htmlContent = (String)((Invocable)scriptEngine).invokeFunction( "renderHtml", uri, initialStateJson); // . enginePool.returnObject(scriptEngine); viewData.put(HTML_CONTENT_KEY, htmlContent); } catch (Throwable e) { enginePool.invalidateObject(scriptEngine); throw e; } } catch (Exception e) { throw new WebApplicationException(e); } } else { viewData.put(HTML_CONTENT_KEY, StringUtils.EMPTY); } // HTML . final String pageContent = StrSubstitutor.replace(templateContent, viewData); entityStream.write(pageContent.getBytes(StandardCharsets.UTF_8)); } /** * javascript. */ private static class ScriptEngineFactory extends BasePooledObjectFactory<AbstractScriptEngine> { @Override public AbstractScriptEngine create() throws Exception { LOG.info("Create new script engine"); // nashorn javascript . final AbstractScriptEngine scriptEngine = (AbstractScriptEngine) new ScriptEngineManager().getEngineByName("nashorn"); try(final InputStreamReader polyfillReader = ResourceUtilities.getResourceTextReader(WEBAPP_ROOT + "server-polyfill.js"); final InputStreamReader serverReader = ResourceUtilities.getResourceTextReader(WEBAPP_ROOT + "static/assets/server.js")) { // , nashorn, . scriptEngine.eval(polyfillReader); // , HTML React. scriptEngine.eval(serverReader); } // . ((Invocable)scriptEngine).invokeFunction( "initializeEngine", ResourceUtilities.class.getName()); return scriptEngine; } @Override public PooledObject<AbstractScriptEngine> wrap(AbstractScriptEngine obj) { return new DefaultPooledObject<AbstractScriptEngine>(obj); } } }
エンジンはJavascriptバージョンECMAScript 5.1を実行し、モジュールのロードをサポートしていません。そのため、クライアントスクリプトと同様に、サーバースクリプトはwebpackを使用してバンドルされます。 サーバーバンドルとクライアントバンドルは共通のコードベースに基づいていますが、エントリポイントが異なります。 何らかの理由で、Nashornは最小化されたバンドル(--optimize-minimizeキーでアセンブルされたwebpack)を実行できません-エラーでクラッシュするため、サーバー側では非最小化バンドルを実行する必要があります。 両方のタイプのバンドルを構築するには、Webpackプラグインunminified-webpack-pluginを同時に使用できます 。
ページの最初のリクエストで、またはエンジンの空きインスタンスがない場合、新しいインスタンスを初期化します。 初期化プロセスでは、Nashornのインスタンスを作成し、その中のクラスパスからロードされたサーバースクリプトを実行します。 Nashornは、setInterval、setTimeoutなどのいくつかの通常のJavaScript関数を実装しないため、最も単純なポリフィルスクリプトを接続する必要があります。 次に、コードが直接ロードされ、HTMLページ(およびクライアント)が形成されます。 このプロセスは非常に高速ではなく、かなり強力なコンピューターでは数秒かかるため、エンジンインスタンスのキャッシュが必要です。
Nashornのポリフィル
// global javascript . var global = this; // window javascript , , // . var window = this; // , Nashorn console. var console = { error: print, debug: print, warn: print, log: print }; // Nashorn setTimeout, callback - . function setTimeout(func, delay) { func(); return 0; }; function clearTimeout() { }; // Nashorn setInterval, callback - . function setInterval(func, delay) { func(); return 0; }; function clearInterval() { };
すでに初期化されたエンジンでのHTMLレンダリングははるかに高速です。 Reactによって生成されたHTMLを取得するには、renderHtml関数を記述します。この関数は、サーバーエントリポイント(src \ server.jsx)に配置します。 現在のURLは、react-routerを使用して処理するためにこの関数に渡され、要求されたページのreduxの初期状態(JSONの形式)が渡されます。 JSONの形式のreduxの同じ状態が、変数window.INITIAL_STATEのページに配置されます。 これは、クライアント上でReactによって構築された要素のツリーがサーバー上で生成されたHTMLと一致するために必要です。
サーバーエントリポイントjsバンドル:
/** * HTML React. * @param {String} url URL . * @param {String} initialStateJson Redux JSON. * @return {String} HTML, React. */ renderHtml = function renderHtml(url, initialStateJson) { // JSON Redux. const initialState = JSON.parse(initialStateJson) // react-router ( ). const history = createMemoryHistory() // Redux , . const store = configureStore(initialState, history, true) // . const htmlContent = {} global.INITIAL_STATE = initialState // URL react-router. match({ routes: routes({history}), location: url }, (error, redirectLocation, renderProps) => { if (error) { throw error } // HTML React. htmlContent.result = ReactDOMServer.renderToString( <AppContainer> <Provider store={store}> <RouterContext {...renderProps}/> </Provider> </AppContainer> ) }) return htmlContent.result }
クライアントjsバンドルエントリポイント:
// Redux. const store = configureStore(initialState, history, false) // HTML, React. const contentElement = document.getElementById("content") // HTML React. ReactDOM.render(<App store={store} history={history}/>, contentElement)
ホットリロードHTML /スタイルのサポート
クライアント側の開発の便宜のために、変更されたページまたはスタイルの「ホット」リロードをサポートするwebpack devサーバーを設定できます。 開発者はアプリケーションを起動し、別のポートでwebpack devサーバーを起動し(package.jsonでnpm run debugコマンドを設定するなど)、ほとんどの場合、変更されたページを更新しないようにします-変更はその場で適用され、これはHTMLコードとスタイルコードの両方に適用されます。 これを行うには、ブラウザーで、以前に構成したwebpack devサーバーのアドレスに移動する必要があります。 サーバーはその場でバンドルを構築し、アプリケーションへの他のリクエストをプロキシします。
package.json:
{ "name": "java-react-redux-isomorphic-example", "version": "1.0.0", "private": true, "scripts": { "debug": "cross-env DEBUG=true APP_PORT=8080 PROXY_PORT=8081 webpack-dev-server --hot --colors --inline", "build": "webpack", "build:debug": "webpack -p" } }
「ホット」リブートを設定するには、以下で説明する手順を実行する必要があります。
webpack設定ファイルで:
- devtoolsでmodule-source-mapまたはmodule-eval-source-mapを指定します。 module-source-mapをオンにすると、デバッグ情報がモジュールの本文に含まれます-この場合、ページが完全にリロードされるとブレークポイントが機能しますが、Chromeデバッグツールでページを変更すると、それぞれ独自のバージョンの重複したモジュールが表示されます。 module-eval-source-mapを有効にした場合、重複はありませんが、一般的なページのリロード中のブレークポイントは機能しません。
devtool: isHot // "" . ? "module-source-map" // "module-eval-source-map" // production. : "source-map"
- devServerで、webpackデバッグサーバーを構成します。「ホット」リブートフラグを設定し、サーバーポートを指定し、アプリケーションへのリクエストのプロキシ設定を指定します。
// . devServer: { // . hot: true, // . port: proxyPort, // . proxy: { "*": `http://localhost:${appPort}` } }
- クライアントスクリプトエントリポイントのエントリで、mediatorモジュールを接続します:react-hot-loader / patch。
entry: { // . main: ["es6-promise", "babel-polyfill"] .concat(isHot // "" - . ? ["react-hot-loader/patch"] // . : []) .concat(["./src/main.jsx"]), // . [isProduction ? "server.min" : "server"]: ["es6-promise", "babel-polyfill", "./src/server.jsx"] }
- publicPath設定の出力で、webpack devサーバーの完全なURLを指定します。
output: { // . path: Path.join(__dirname, "../resources/webapp/static/assets/"), publicPath: isHot // "" ( ). ? `http://localhost:${proxyPort}/assets/` : "/assets/", filename: "[name].js", chunkFilename: "[name].js" }
- babelブートローダー設定で、プラグインを接続して「ホット」リブートをサポートします:syntax-dynamic-importとreact-hot-loader / babel。
{ // JavaScript (Babel). test: /\.(js|jsx)?$/, exclude: /(node_modules)/, use: [ { loader: isHot // "" babel. ? "babel-loader?plugins[]=syntax-dynamic-import,plugins[]=react-hot-loader/babel" : "babel-loader" } ] }
- スタイルローダーの設定で、スタイルローダーローダーの使用を指定します。 この場合、スタイルはjavascriptコードでインラインになります。 スタイルの「ホット」リロードが無効になっている場合(実稼働環境など)、extract-text-webpack-pluginを使用したスタイルバンドルの形成が使用されます。
{ // CSS. test: /\.css$/, use: isHot // "" JavaScript . ? ["style-loader"].concat(cssStyles) // production - . : ExtractTextPlugin.extract({use: cssStyles, publicPath: "../assets/"}) }
- Webpack.NamedModulesPluginプラグインを接続して、名前付きモジュールを作成します。
アプリケーションへのクライアントエントリポイントで、モジュール更新ハンドラーを挿入します。 ハンドラーは更新されたモジュールをダウンロードし、Reactを使用してHTMLレンダリングプロセスを開始します。
// HTML React. ReactDOM.render(<App store={store} history={history}/>, contentElement) if (module.hot) { // "" . module.hot.accept("./containers/app", () => { const app = require("./containers/app").default ReactDOM.render(app({store, history}), contentElement) }) }
reduxリポジトリが作成されるモジュールに、モジュール更新ハンドラーを挿入します。 このハンドラーは、更新されたreduxコンバーターをロードし、古いコンバーターをそれらに置き換えます。
const store = createStore(reducers, initialState, applyMiddleware(...middleware)) if (module.hot) { // "" Redux-. module.hot.accept("./reducers", () => { const nextRootReducer = require("./reducers") store.replaceReducer(nextRootReducer) }) } return store
Javaアプリケーション自体では、frontend-maven-pluginとReactのサーバーサイドレンダリングを使用してバンドルの構築を無効にする必要があります:webpack devサーバーはスクリプトとスタイルバンドルの構築を担当します。バンドル。 frontend-maven-pluginとサーバー側のReactレンダリングを使用して再構築を無効にするには、mavenプロファイル:frontendDevelopment(mavenとの統合をサポートするIDEに含めることができます)を提供できます。 必要に応じて、バンドルはwebpackを使用していつでも手動で再構築されます。