
Для создания изоморфных приложений на React обычно используется Node.js в качестве серверной части. Но, если сервер пишется на Java, то не стоит отказываться от изоморфного приложения: в Java входит встроенный javascript движок (Nashorn), который вполне справится с серверным рендерингом HTML с помощью React.
Код приложения, демонстрирующего серверный рендеринг React с сервером на Java, находится на GitHub. В статье буду рассмотрены:
- Сервер на Java в стиле микросервиса на основе Netty и JAX-RS (в реализации Resteasy) для обработки web-запросов, с возможностью запуска в Docker.
- Dependency Injection с использованием библиотеки CDI (в реализации Weld SE).
- Сборка javascript бандла с помощью Webpack 2.
- Настройка редеринга HTML на сервере с помощью React.
- Запуск отладки с поддержкой «горячей» перезагрузки страниц и стилей с использованием Webpack dev server.
Сервер на Java
Рассмотрим создание сервера на Java в стиле микросервиса (самодостаточный запускаемый jar, не требующий использования каких-либо сервлет-контейнеров). В качестве библиотеки для управления зависимостями будем использовать стандарт CDI (Contexts and Dependency Injection), который пришел из мира Java EE, но вполне может использоваться в приложениях Java SE. Реализация CDI — Weld SE — это мощная и отлично документированная библиотека для управления зависимостями. Для CDI существует множество биндингов к другим библиотекам, например, в приложении используются CDI биндинги для JAX-RS и Netty. Достаточно в каталоге src/main/resources/META-INF создать файл beans.xml (декларация, что этот модуль поддерживает CDI), разметить классы стандартными атрибутами, инициализировать контейнер и можно инжектить зависимости. Классы, помеченные специальными аннотациями зарегистрируются автоматически (доступна и ручная регистрация).
// Стартовый метод. 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; } }
Обработку web запросов настроим через встроенный web-сервер — 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 достаточно указать, что в качестве компонентов JAX-RS будут использоваться зарегистрированные в CDI обработчики запросов (помеченные аннотацией JAX-RS: Path) и другие компоненты JAX-RS, которые называются провайдерами (помеченные аннотацией JAX-RS: Provider). Более подробно о Resteasy можно узнать из документации.
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, картинки) разместим в classpath (src/main/resources/webapp), они поместятся в результирующий jar файл. Для доступа к таким файлам используется обработчик URL вида {fileName:.*}.{ext}, который загружает файл из classpath и отдает клиенту.
/** * Обработчик запросов к статическим файлам. * <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(); } }
Серверный рендеринг HTML на React
Для сборки бандлов при построении Java приложения можно использовать maven плагин frontend-maven-plugin. Он самостоятельно загружает и локально сохраняет NodeJs нужной версии, строит бандлы с помощью webpack. Достаточно запускать обычное построение Java проекта командой mvn (либо в IDE, которая поддерживает интеграцию с maven). Клиентский javascript, стили, package.json, файл конфигурации webpack разместим в каталоге src/main/frontend, результирующий бандл в src/main/resources/webapp/static/assets.
<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>
Чтобы настроить собственный генератор HTML страниц в JAX-RS нужно создать какой нибудь класс, создать для него обработчик с аннотаций Provider, реализующий интерфейс 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 не может исполнять минимизированый бандл (собираемый webpack с ключом —optimize-minimize) — падает с ошибкой, поэтому на стороне сервера нужно исполнять неминимизированный бандл. Для построения обоих типов бандлов одновременно можно использовать плагин к Webpack: unminified-webpack-plugin.
При первом запросе любой страницы, либо если нет свободного экземпляра движка, сделаем инициализацию нового экземпляра. Процесс инициализации состоит из создания экземпляра Nashorn и исполнения в нем серверных скриптов, загружаемых из classpath. Nashorn не реализует несколько обычных javascript функций, таких как setInterval, setTimeout, поэтому нужно подключать простейший скрипт-polyfill. Затем загружается непосредственно код, который формирует HTML страницы (так же как и на клиенте). Этот процесс не очень быстрый, на достаточно мощном компьютере занимает пару секунд, таким образом нужен кеш экземпляров движков.
// Инициализация объекта 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 на уже проинициализированном движке происходит гораздо быстрее. Для получения HTML, сформированного React, напишем функцию renderHtml, которую поместим в серверную точку входа (src\server.jsx). В эту функцию передается текущий URL, для обработки его с помощью react-router, и начальное состояние redux для запрошенной страницы (в виде JSON). То же самое состояние для redux, в виде JSON, помещается на страницу в переменную 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 server с поддержкой «горячей» перезагрузки изменившихся страниц или стилей. Разработчик запускает приложение, запускает webpack dev server на другом порту (например, настроив в 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}` } } - В entry для точки входа клиентского скрипта подключить модуль — медиатор: 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"] } - В output в настройке publicPath указать полный URL webpack dev сервера.
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" } ] } - В настройках загрузчика стилей указать использования загрузчика style-loader. В этом случае стили будут инлайнится в javascript код. При отключенной «горячей» перезагрузки стилей (например в production) используется формирование бандла стилей с помощью extract-text-webpack-plugin.
{ // Загрузчик стилей CSS. test: /\.css$/, use: isHot // При использовании "горячей" перезагрузки стили помещаются в бандл с JavaScript кодом. ? ["style-loader"].concat(cssStyles) // В production - стили это отдельный бандл. : ExtractTextPlugin.extract({use: cssStyles, publicPath: "../assets/"}) } - Подключить плагин Webpack.NamedModulesPlugin для формирования именованных модулей.
В клиентской точке входа в приложение вставить обработчик обновления модуля. Обработчик загружает обновленный модуль и запускает процесс рендеринга HTML с помощью React.
// Выполнение рендеринга 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 server, он делает это очень быстро и в памяти, процессор и диск не будут нагружаться перестроением бандлов. Для отключения пересборки с помощью frontend-maven-plugin и серверного рендеринга React можно предусмотреть профиль maven: frontendDevelopment (его можно включить в IDE, которая поддерживает интеграцию с maven). При необходимости, бандлы пересобираются вручную в любой момент с помощью webpack.
ссылка на оригинал статьи https://habrahabr.ru/post/327480/
Добавить комментарий