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