Java и isomorphic React

в 8:58, , рубрики: cdi, isomorphic, java, javascript, JAX-RS, ReactJS, Resteasy, weld

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, которую поместим в серверную точку входа (srcserver.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.

Автор: complit-s

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js