Javaと同形React

画像



React同型アプリケーションを作成するには、通常、Node.jsをサーバー側として使用します。 ただし、サーバーがJavaで作成されている場合は、同形アプリケーションを放棄しないでください。Javaには、Reactを使用したHTMLのサーバー側レンダリングに対応できる組み込みJavaScriptエンジン(Nashorn)が含まれています。



Javaサーバーを使用したサーバー側のReactレンダリングを示すアプリケーションのコードはGitHubにあります 。 記事で私は考慮します:





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設定ファイルで:





アプリケーションへのクライアントエントリポイントで、モジュール更新ハンドラーを挿入します。 ハンドラーは更新されたモジュールをダウンロードし、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を使用していつでも手動で再構築されます。



All Articles