Java и isomorphic React

от автора

image

Для создания изоморфных приложений на 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 можно узнать из документации.

Netty и JAX-RS Application

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.

Настройка 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> 

Чтобы настроить собственный генератор 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 страницы (так же как и на клиенте). Этот процесс не очень быстрый, на достаточно мощном компьютере занимает пару секунд, таким образом нужен кеш экземпляров движков.

Полифил для 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 на уже проинициализированном движке происходит гораздо быстрее. Для получения 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *