[Из песочницы] 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 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> getClasses() {
        final Set> result = new HashSet<>();
 
        // Из расширения CDI для Resteasy берется информация об обработчиках запросов JAX-RS.
        result.addAll((Collection>) (Object)extension.getResources());
        // Из расширения CDI для Resteasy берется информация о провайдерах JAX-RS.     
        result.addAll((Collection>) (Object)extension.getProviders());
        return result;
    }
}


Все статические файлы (бандлы javascript, css, картинки) разместим в classpath (src/main/resources/webapp), они поместятся в результирующий jar файл. Для доступа к таким файлам используется обработчик URL вида {fileName:.*}.{ext}, который загружает файл из classpath и отдает клиенту.
Обработчик запросов к статике
/**
 * Обработчик запросов к статическим файлам.
 * 

Запросом статического файла считается любой запрос вида {filename}.{ext}

*/ @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

    com.github.eirslett
    frontend-maven-plugin
    
        v${node.version}
        ${npm.version}
        ${basedir}/src/main/frontend
        ${basedir}/src/main/frontend
    
    
        
        
            nodeInstall
            
                install-node-and-npm
            
             
        
        
            npmInstall
            
                npm
                                  
        
        
        
                webpackBuild
                
                    webpack
                
                
                    ${webpack.skip}
                    ${webpack.arguments}
                    ${basedir}/src/main/frontend/app
                    ${basedir}/src/main/resources/webapp/static/assets
                    
                        ${basedir}/src/main/frontend/webpack.config.js
                        ${basedir}/src/main/frontend/package.json
                    
                
            
                        



Чтобы настроить собственный генератор 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 viewData = new HashMap<>();
     
    private final Map reduxInitialState = new HashMap<>();
 
    ..............
}
 
/**
 * Обработка данных страницы, заполненных в {@link ViewResult} и отправка HTML.
 * 

* Если в конфигурации включено использование React в качестве движка для рендеринга HTML (React Isomorphic), * то в шаблон страницы включается контент, сформированный с помощью React. *

*/ @Provider @ApplicationScoped public class ViewResultBodyWriter implements MessageBodyWriter { .............. private ObjectPool 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(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 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 { @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 wrap(AbstractScriptEngine obj) { return new DefaultPooledObject(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(
      
        
          
        
      
    )
  })
 
  return htmlContent.result
}

Клиентская точка входа js бандла:
// Создание хранилища Redux.
const store = configureStore(initialState, history, false)
// Элемент в который нужно вставлять HTML, сформированный React.
const contentElement = document.getElementById("content")
 
// Выполнение рендеринга HTML с помощью React.
ReactDOM.render(, 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(, 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.

Комментарии (0)

© Habrahabr.ru