1. Обзор
В этом уроке мы поймем, что такое изоморфное приложение. Мы также обсудим Nashorn , движок JavaScript в комплекте с Java.
Кроме того, мы рассмотрим, как мы можем использовать Nashorn вместе с внешней библиотекой, такой как React , для создания изоморфного приложения.
2. Немного истории
Традиционно клиентские и серверные приложения были написаны таким образом, что серверная сторона была довольно тяжелой. Думайте о PHP как о механизме сценариев, генерирующем в основном статический HTML, а веб-браузеры отображают их.
Браузер Netscape появился с поддержкой JavaScript еще в середине девяностых . Это начало смещать часть обработки с серверной стороны на клиентский браузер. Долгое время разработчики боролись с различными проблемами, связанными с поддержкой JavaScript в веб-браузерах.
С растущим спросом на более быстрый и интерактивный пользовательский интерфейс границы уже раздвинулись. Одним из первых фреймворков, изменивших правила игры, был jQuery . Он принес несколько удобных для пользователя функций и значительно расширил поддержку AJAX.
Вскоре стало появляться множество фреймворков для фронтенд-разработки, что значительно улучшило опыт разработчика. Начиная с AngularJS от Google, React от Facebook и позже Vue, они начали привлекать внимание разработчиков.
Благодаря поддержке современных браузеров, замечательным платформам и необходимым инструментам тенденции в значительной степени смещаются в сторону клиентской части .
Иммерсивный опыт на все более быстрых портативных устройствах требует большей обработки на стороне клиента.
3. Что такое изоморфное приложение?
Итак, мы увидели, как интерфейсные фреймворки помогают нам разрабатывать веб-приложения, в которых пользовательский интерфейс полностью визуализируется на стороне клиента.
Однако также можно использовать ту же структуру на стороне сервера и создать такой же пользовательский интерфейс.
Теперь нам не обязательно придерживаться только решений на стороне клиента или только на стороне сервера. Лучше иметь решение, в котором клиент и сервер могут обрабатывать один и тот же интерфейсный код и генерировать один и тот же пользовательский интерфейс.
У этого подхода есть преимущества, которые мы обсудим позже.
Такие веб-приложения называются изоморфными или универсальными . Теперь языком клиентской части является исключительно JavaScript. Следовательно, чтобы изоморфное приложение работало, мы также должны использовать JavaScript на стороне сервера.
Node.js на сегодняшний день является наиболее распространенным выбором для создания приложения с рендерингом на стороне сервера.
4. Что такое Нашорн?
Итак, как же подходит Nashorn и почему мы должны его использовать? Nashorn — это движок JavaScript, по умолчанию упакованный с Java . Следовательно, если у нас уже есть серверная часть веб-приложения на Java и мы хотим создать изоморфное приложение, Nashorn очень удобен !
Nashorn был выпущен как часть Java 8. Он в первую очередь ориентирован на разрешение встроенных приложений JavaScript в Java.
Nashorn компилирует JavaScript в памяти в Java Bytecode и передает его JVM для выполнения. Это обеспечивает лучшую производительность по сравнению с более ранним двигателем Rhino.
5. Создание изоморфного приложения
Мы рассмотрели достаточно контекста. Наше приложение здесь будет отображать последовательность Фибоначчи и предоставлять кнопку для генерации и отображения следующего числа в последовательности. Давайте теперь создадим простое изоморфное приложение с бэкендом и интерфейсом:
- Интерфейс: простой интерфейс на основе React.js.
- Серверная часть: простая серверная часть Spring Boot с Nashorn для обработки JavaScript.
6. Интерфейс приложения
Мы будем использовать React.js для создания внешнего интерфейса . React — это популярная библиотека JavaScript для создания одностраничных приложений. Это помогает нам разложить сложный пользовательский интерфейс на иерархические компоненты с необязательным состоянием и односторонней привязкой данных.
React анализирует эту иерархию и создает в памяти структуру данных, называемую виртуальной DOM. Это помогает React находить изменения между различными состояниями и вносить минимальные изменения в DOM браузера.
6.1. Реагировать Компонент
Давайте создадим наш первый компонент React:
var App = React.createClass({displayName: "App",
handleSubmit: function() {
var last = this.state.data[this.state.data.length-1];
var secondLast = this.state.data[this.state.data.length-2];
$.ajax({
url: '/next/'+last+'/'+secondLast,
dataType: 'text',
success: function(msg) {
var series = this.state.data;
series.push(msg);
this.setState({data: series});
}.bind(this),
error: function(xhr, status, err) {
console.error('/next', status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.setState({data: this.props.data});
},
getInitialState: function() {
return {data: []};
},
render: function() {
return (
React.createElement("div", {className: "app"},
React.createElement("h2", null, "Fibonacci Generator"),
React.createElement("h2", null, this.state.data.toString()),
React.createElement("input", {type: "submit", value: "Next", onClick: this.handleSubmit})
)
);
}
});
Теперь давайте разберемся, что делает приведенный выше код:
- Для начала мы определили компонент класса в React под названием «Приложение».
- Наиболее важной функцией внутри этого компонента является «рендеринг» , который отвечает за генерацию пользовательского интерфейса.
- Мы предоставили имя класса стиля
,
которое может использовать компонент. - Здесь мы используем состояние компонента для хранения и отображения серии.
- Хотя состояние инициализируется как пустой список, оно извлекает данные, передаваемые компоненту в качестве реквизита, когда компонент монтируется.
- Наконец, при нажатии кнопки «Добавить» выполняется jQuery-вызов службы REST.
- Вызов извлекает следующее число в последовательности и добавляет его к состоянию компонента.
- Изменение состояния компонента автоматически перерисовывает компонент.
6.2. Использование компонента React
React ищет именованный элемент «div» на HTML-странице, чтобы закрепить его содержимое . Все, что нам нужно сделать, это предоставить HTML-страницу с этим элементом «div» и загрузить файлы JS:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Hello React</title>
<script type="text/javascript" src="js/react.js"></script>
<script type="text/javascript" src="js/react-dom.js"></script>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript">
ReactDOM.render(
React.createElement(App, {data: [0,1,1]}),
document.getElementById("root")
);
</script>
</body>
</html>
Итак, давайте посмотрим, что мы здесь сделали:
- Мы импортировали необходимые библиотеки JS, react, react-dom и jQuery
- После этого мы определили элемент «div» с именем «root».
- Мы также импортировали файл JS с нашим компонентом React.
- Затем мы назвали компонент React «Приложение» с некоторыми начальными данными, первыми тремя числами Фибоначчи.
7. Серверная часть приложения
Теперь давайте посмотрим, как мы можем создать подходящую серверную часть для нашего приложения. Мы уже решили использовать Spring Boot вместе с Spring Web для создания этого приложения. Что еще более важно, мы решили использовать Nashorn для обработки внешнего интерфейса на основе JavaScript, который мы разработали в предыдущем разделе.
7.1. Зависимости Maven
Для нашего простого приложения мы будем использовать JSP вместе со Spring MVC, поэтому мы добавим пару зависимостей в наш POM:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
Первая — это стандартная весенняя загрузочная зависимость для веб-приложения. Второй нужен для компиляции JSP .
7.2. Веб-контроллер
Давайте теперь создадим наш веб-контроллер, который будет обрабатывать наш файл JavaScript и возвращать HTML с помощью JSP:
@Controller
public class MyWebController {
@RequestMapping("/")
public String index(Map<String, Object> model) throws Exception {
ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");
nashorn.eval(new FileReader("static/js/react.js"));
nashorn.eval(new FileReader("static/js/react-dom-server.js"));
nashorn.eval(new FileReader("static/app.js"));
Object html = nashorn.eval(
"ReactDOMServer.renderToString(" +
"React.createElement(App, {data: [0,1,1]})" +
");");
model.put("content", String.valueOf(html));
return "index";
}
}
Итак, что именно здесь происходит:
- Мы получаем экземпляр
ScriptEngine
типа Nashorn изScriptEngineManager .
- Затем мы загружаем соответствующие библиотеки в React, react.js и react-dom-server.js.
- Мы также загружаем наш JS-файл, в котором есть наш реагирующий компонент «Приложение».
- Наконец, мы оцениваем фрагмент JS, создающий элемент реакции с компонентом «Приложение» и некоторыми начальными данными.
- Это дает нам вывод React, фрагмент HTML как
Object
- Мы передаем этот HTML-фрагмент в качестве данных соответствующему представлению — JSP.
7.3. JSP
Теперь, как мы обработаем этот HTML-фрагмент в нашем JSP?
Напомним, что React автоматически добавляет свой вывод в элемент с именем «div» — в нашем случае «root». Однако мы добавим сгенерированный на стороне сервера HTML-фрагмент к тому же элементу вручную в нашем JSP.
Давайте посмотрим, как теперь выглядит JSP:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Hello React!</title>
<script type="text/javascript" src="js/react.js"></script>
<script type="text/javascript" src="js/react-dom.js"></script>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
</head>
<body>
<div id="root">${content}</div>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript">
ReactDOM.render(
React.createElement(App, {data: [0,1,1]}),
document.getElementById("root")
);
</script>
</body>
</html>
Это та же самая страница, которую мы создали ранее, за исключением того факта, что мы добавили наш HTML-фрагмент в «корневой» div, который ранее был пуст.
7.4. REST-контроллер
Наконец, нам также нужна конечная точка REST на стороне сервера, которая дает нам следующее число Фибоначчи в последовательности:
@RestController
public class MyRestController {
@RequestMapping("/next/{last}/{secondLast}")
public int index(
@PathVariable("last") int last,
@PathVariable("secondLast") int secondLast) throws Exception {
return last + secondLast;
}
}
Здесь нет ничего особенного, просто простой контроллер Spring REST.
8. Запуск приложения
Теперь, когда мы завершили как внешний, так и внутренний интерфейс, пришло время запустить приложение.
Мы должны запустить приложение Spring Boot в обычном режиме, используя класс начальной загрузки:
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
Когда мы запускаем этот класс, Spring Boot компилирует наши JSP и делает их доступными во встроенном Tomcat вместе с остальной частью веб-приложения.
Теперь, если мы зайдем на наш сайт, мы увидим:
Давайте поймем последовательность событий:
- Браузер запрашивает эту страницу
- Когда поступает запрос на эту страницу, веб-контроллер Spring обрабатывает файлы JS.
- Механизм Nashorn генерирует фрагмент HTML и передает его в JSP.
- JSP добавляет этот HTML-фрагмент в «корневой» элемент div, возвращая, наконец, приведенную выше HTML-страницу.
- Браузер отображает HTML, тем временем начинает загружать файлы JS.
- Наконец, страница готова для действий на стороне клиента — мы можем добавить больше чисел в серию.
Здесь важно понять, что произойдет, если React найдет фрагмент HTML в целевом элементе «div». В таких случаях React сравнивает этот фрагмент с тем, что у него есть, и не заменяет его, если находит разборчивый фрагмент . Это именно то, что обеспечивает рендеринг на стороне сервера и изоморфные приложения.
9. Что еще возможно?
В нашем простом примере мы только коснулись того, что возможно. Интерфейсные приложения с современными платформами на основе JS становятся все более мощными и сложными. С этой дополнительной сложностью нам нужно позаботиться о многих вещах:
- Мы создали только один компонент React в нашем приложении, тогда как на самом деле это может быть несколько компонентов, образующих иерархию , которые передают данные через свойства .
- Мы хотели бы создать отдельные JS-файлы для каждого компонента , чтобы они оставались управляемыми и управляли их зависимостями с помощью «экспорта/требования» или «экспорта/импорта».
- Более того, может оказаться невозможным управлять состоянием только внутри компонентов; мы можем захотеть использовать библиотеку управления состоянием, такую как Redux
- Кроме того, нам, возможно, придется взаимодействовать с внешними службами в качестве побочных эффектов действий; это может потребовать от нас использования шаблона, такого как redux-thunk или Redux-Saga .
- Самое главное, мы хотели бы использовать JSX, синтаксическое расширение JS для описания пользовательского интерфейса.
Хотя Nashorn полностью совместим с чистым JS, он может не поддерживать все функции, упомянутые выше. Многие из них требуют транскомпиляции и полифиллов из-за совместимости с JS.
Обычной практикой в таких случаях является использование сборщика модулей, такого как Webpack или Rollup . В основном они обрабатывают все исходные файлы React и объединяют их в один файл JS вместе со всеми зависимостями. Это неизменно требует современного компилятора JavaScript, такого как Babel , для компиляции JavaScript с обратной совместимостью.
В финальном пакете есть только старый добрый JS, который понимают браузеры и которого придерживается Nashorn.
10. Преимущества изоморфного приложения
Итак, мы много говорили об изоморфных приложениях и даже создали простое приложение. Но почему именно мы должны заботиться об этом? Давайте разберемся с некоторыми ключевыми преимуществами использования изоморфного приложения.
10.1. Рендеринг первой страницы
Одним из наиболее значительных преимуществ изоморфного приложения является более быстрая отрисовка первой страницы . В типичном клиентском приложении браузер начинает с загрузки всех артефактов JS и CSS.
После этого они загружаются и начинают рендерить первую страницу. Если мы отправим первую страницу, обработанную на стороне сервера, это может быть намного быстрее, обеспечивая улучшенный пользовательский интерфейс.
10.2. SEO дружественный
Еще одно преимущество, часто упоминаемое в отношении рендеринга на стороне сервера, связано с SEO . Считается, что поисковые боты не могут обрабатывать JavaScript и, следовательно, не видят индексную страницу, отображаемую на стороне клиента с помощью таких библиотек, как React. Таким образом, страница, отображаемая на стороне сервера, более удобна для SEO. Однако стоит отметить, что современные поисковые роботы утверждают, что обрабатывают JavaScript.
11. Заключение
В этом руководстве мы рассмотрели основные понятия изоморфных приложений и движка Nashorn JavaScript. Далее мы рассмотрели, как создать изоморфное приложение с помощью Spring Boot, React и Nashorn.
Затем мы обсудили другие возможности расширения интерфейсного приложения и преимущества использования изоморфного приложения.
Как всегда, код можно найти на GitHub .