Isomorphe Anwendung mit React und Nashorn

1. Übersicht

In diesem Tutorial werden wir verstehen, was genau eine isomorphe App ist. Wir werden auch Nashorn diskutieren, die mit Java gebündelte JavaScript-Engine.

Darüber hinaus werden wir untersuchen, wie wir Nashorn zusammen mit einer Front-End-Bibliothek wie React verwenden können, um eine isomorphe App zu erstellen.

2. Ein bisschen Geschichte

Traditionell wurden Client- und Serveranwendungen auf eine Weise geschrieben, die auf der Serverseite ziemlich schwer war. Stellen Sie sich PHP als eine Skript-Engine vor, die hauptsächlich statisches HTML generiert und von Webbrowsern gerendert wird.

Netscape wurde bereits Mitte der neunziger Jahre von JavaScript in seinem Browser unterstützt . Dadurch wurde ein Teil der Verarbeitung vom serverseitigen zum clientseitigen Browser verschoben. Entwickler hatten lange Zeit mit verschiedenen Problemen bezüglich der JavaScript-Unterstützung in Webbrowsern zu kämpfen.

Mit der wachsenden Nachfrage nach einer schnelleren und interaktiveren Benutzererfahrung wurde die Grenze bereits verschärft. Eines der frühesten Frameworks, das das Spiel verändert hat, war jQuery. Es brachte mehrere benutzerfreundliche Funktionen und eine stark verbesserte Unterstützung für AJAX.

Bald tauchten viele Frameworks für die Front-End-Entwicklung auf, was die Erfahrung des Entwicklers erheblich verbesserte. Beginnend mit AngularJS von Google, React von Facebook und später Vue erregten sie die Aufmerksamkeit der Entwickler.

Mit moderner Browserunterstützung, bemerkenswerten Frameworks und erforderlichen Tools verschieben sich die Gezeiten weitgehend in Richtung Client .

Ein umfassendes Erlebnis auf immer schnelleren Handheld-Geräten erfordert eine stärkere clientseitige Verarbeitung.

3. Was ist eine isomorphe App?

Wir haben also gesehen, wie Front-End-Frameworks uns bei der Entwicklung einer Webanwendung helfen, bei der die Benutzeroberfläche auf der Clientseite vollständig gerendert wird.

Allerdings ist es auch möglich , den gleichen Rahmen auf der Server-Seite zu verwenden und die gleiche Benutzeroberfläche zu erzeugen.

Jetzt müssen wir uns nicht unbedingt nur an clientseitige oder serverseitige Lösungen halten. Ein besserer Weg ist eine Lösung, bei der Client und Server denselben Front-End-Code verarbeiten und dieselbe Benutzeroberfläche generieren können.

Dieser Ansatz bietet Vorteile, auf die wir später noch eingehen werden.

Solche Webanwendungen werden als isomorph oder universell bezeichnet . Jetzt ist die clientseitige Sprache ausschließlich JavaScript. Damit eine isomorphe App funktioniert, müssen wir auch auf der Serverseite JavaScript verwenden.

Node.js ist bei weitem die häufigste Wahl, um eine serverseitig gerenderte Anwendung zu erstellen.

4. Was ist Nashorn?

Also, wo passt Nashorn hin und warum sollten wir es verwenden? Nashorn ist eine JavaScript-Engine, die standardmäßig mit Java gepackt ist . Wenn wir bereits ein Webanwendungs-Backend in Java haben und eine isomorphe App erstellen möchten, ist Nashorn ziemlich praktisch!

Nashorn wurde als Teil von Java 8 veröffentlicht. Dies konzentriert sich hauptsächlich auf das Zulassen eingebetteter JavaScript-Anwendungen in Java.

Nashorn kompiliert JavaScript im Arbeitsspeicher in Java Bytecode und übergibt es zur Ausführung an die JVM. Dies bietet eine bessere Leistung im Vergleich zum früheren Motor Rhino.

5. Erstellen einer isomorphen App

Wir haben jetzt genug Kontext durchlaufen. Unsere Anwendung hier zeigt eine Fibonacci-Sequenz an und bietet eine Schaltfläche zum Generieren und Anzeigen der nächsten Nummer in der Sequenz. Erstellen wir jetzt eine einfache isomorphe App mit Back-End und Front-End:

  • Frontend: Ein einfaches React.js-basiertes Frontend
  • Backend: Ein einfaches Spring Boot-Backend mit Nashorn zur Verarbeitung von JavaScript

6. Anwendungs-Frontend

Wir werden React.js verwenden, um unser Frontend zu erstellen . React ist eine beliebte JavaScript-Bibliothek zum Erstellen von Apps mit nur einer Seite. Es hilft uns, eine komplexe Benutzeroberfläche in hierarchische Komponenten mit optionalem Status und einseitiger Datenbindung zu zerlegen .

React analysiert diese Hierarchie und erstellt eine speicherinterne Datenstruktur namens Virtual DOM. Dies hilft React, Änderungen zwischen verschiedenen Zuständen zu finden und minimale Änderungen am Browser-DOM vorzunehmen.

6.1. Komponente reagieren

Lassen Sie uns unsere erste React-Komponente erstellen:

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}) ) ); } });

Lassen Sie uns nun verstehen, was der obige Code bewirkt:

  • Zunächst haben wir in React eine Klassenkomponente namens "App" definiert.
  • Die wichtigste Funktion in dieser Komponente ist "Rendern" , das für die Generierung der Benutzeroberfläche verantwortlich ist
  • Wir haben einen Stil className angegeben , den die Komponente verwenden kann
  • Wir verwenden hier den Komponentenstatus, um die Serien zu speichern und anzuzeigen
  • Während der Status als leere Liste initialisiert wird, werden Daten abgerufen, die als Requisite an die Komponente übergeben werden, wenn die Komponente bereitgestellt wird
  • Schließlich wird durch Klicken auf die Schaltfläche „Hinzufügen“ ein jQuery-Aufruf an den REST-Service gesendet
  • Der Aufruf ruft die nächste Nummer in der Sequenz ab und hängt sie an den Status der Komponente an
  • Wenn Sie den Status der Komponente ändern, wird die Komponente automatisch neu gerendert

6.2. Verwenden der Reaktionskomponente

React sucht in der HTML-Seite nach einem benannten "div" -Element, um dessen Inhalt zu verankern . Alles was wir tun müssen, ist eine HTML-Seite mit diesem "div" -Element bereitzustellen und die JS-Dateien zu laden:

   Hello React ReactDOM.render( React.createElement(App, {data: [0,1,1]}), document.getElementById("root") );   

Mal sehen, was wir hier gemacht haben:

  • We imported the required JS libraries, react, react-dom and jQuery
  • After that, we defined a “div” element called “root”
  • We also imported the JS file with our React component
  • Next, we called the React component “App” with some seed data, the first three Fibonacci numbers

7. Application Back-End

Now, let's see how we can create a fitting back-end for our application. We've already decided to use Spring Boot along with Spring Web for building this application. More importantly, we've decided to use Nashorn to process the JavaScript-based front-end we developed in the last section.

7.1. Maven Dependencies

For our simple application, we'll be using JSP together with Spring MVC, so we'll add a couple of dependencies to our POM:

 org.springframework.boot spring-boot-starter-web   org.apache.tomcat.embed tomcat-embed-jasper provided 

The first one is the standard spring boot dependency for a web application. The second one is needed to compile JSPs.

7.2. Web Controller

Let's now create our web controller, which will process our JavaScript file and return an HTML using JSP:

@Controller public class MyWebController { @RequestMapping("/") public String index(Map 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"; } }

So, what exactly is happening here:

  • We fetch an instance of ScriptEngine of type Nashorn from ScriptEngineManager
  • Then, we load relevant libraries to React, react.js, and react-dom-server.js
  • We also load our JS file that has our react component “App”
  • Finally, we evaluate a JS fragment creating react element with the component “App” and some seed data
  • This provides us with an output of React, an HTML fragment as Object
  • We pass this HTML fragment as data to the relevant view – the JSP

7.3. JSP

Now, how do we process this HTML fragment in our JSP?

Recall that React automatically adds its output to a named “div” element – “root” in our case. However, we'll add our server-side generated HTML fragment to the same element manually in our JSP.

Let's see how the JSP looks now:

   Hello React! ${content} ReactDOM.render( React.createElement(App, {data: [0,1,1]}), document.getElementById("root") );   

This is the same page we created earlier, except for the fact that we've added our HTML fragment into the “root” div, which was empty earlier.

7.4. REST Controller

Finally, we also need a server-side REST endpoint that gives us the next Fibonacci number in the sequence:

@RestController public class MyRestController { @RequestMapping("/next/{last}/{secondLast}") public int index( @PathVariable("last") int last, @PathVariable("secondLast") int secondLast) throws Exception { return last + secondLast; } }

Nothing fancy here, just a simple Spring REST controller.

8. Running the Application

Now, that we have completed our front-end as well as our back-end, it's time to run the application.

We should start the Spring Boot application normally, making use of the bootstrapping class:

@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); } }

When we run this class, Spring Boot compiles our JSPs and makes them available on embedded Tomcat along with the rest of the web application.

Now, if we visit our site, we'll see:

Let's understand the sequence of events:

  • The browser requests this page
  • When the request for this page arrives, Spring web controller process the JS files
  • Nashorn engine generates an HTML fragment and passes this to the JSP
  • JSP adds this HTML fragment to the “root” div element, finally returning the above HTML page
  • The browser renders the HTML, meanwhile starts downloading JS files
  • Finally, the page is ready for client-side actions — we can add more numbers in the series

The important thing to understand here is what happens if React finds an HTML fragment in the target “div” element. In such cases, React compares this fragment with what it has and does not replace it if it finds a legible fragment. This is exactly what powers server-side rendering and isomorphic apps.

9. What More Is Possible?

In our simple example, we have just scratched the surface of what's possible. Front-end applications with modern JS-based frameworks are getting increasingly more powerful and complex. With this added complexity, there are many things that we need to take care of:

  • We've created just one React component in our application when in reality, this can be several components forming a hierarchy which pass data through props
  • We would like to create separate JS files for every component to keep them manageable and manage their dependencies through “exports/require” or “export/import”
  • Moreover, it may not be possible to manage state within components only; we may want to use a state management library like Redux
  • Furthermore, we may have to interact with external services as side-effects of actions; this may require us to use a pattern like redux-thunk or Redux-Saga
  • Most importantly, we would want to leverage JSX, a syntax extension to JS for describing the user interface

While Nashorn is fully compatible with pure JS, it may not support all the features mentioned above. Many of these require trans-compiling and polyfills due to JS compatibility.

The usual practice in such cases is to leverage a module bundler like Webpack or Rollup. What they mainly do is to process all of React source files and bundle them into a single JS file along with all dependencies. This invariably requires a modern JavaScript compiler like Babel to compile JavaScript to be backward compatible.

The final bundle only has good old JS, which browsers can understand and Nashorn adheres to as well.

10. Benefits of an Isomorphic App

So, we've talked a great deal about isomorphic apps and have even created a simple application now. But why exactly should we even care about this? Let's understand some of the key benefits of using an isomorphic app.

10.1. First Page Rendering

One of the most significant benefits of an isomorphic app is the faster rendering of the first page. In the typical client-side rendered application, the browser begins by downloading all the JS and CSS artifacts.

After that, they load and start rendering the first page. If we send the first page rendered from the server-side, this can be much faster, providing an enhanced user experience.

10.2. SEO Friendly

Another benefit often cited with server-side rendering is related to SEO. It's believed that search bots are not able to process JavaScript and hence do not see an index page rendered at client-side through libraries like React. A server-side rendered page, therefore, is SEO friendlier. It's worth noting, though, that Modern search engine bots claim to process JavaScript.

11. Schlussfolgerung

In diesem Tutorial haben wir die Grundkonzepte isomorpher Anwendungen und die Nashorn JavaScript-Engine durchgearbeitet. Wir haben weiter untersucht, wie eine isomorphe App mit Spring Boot, React und Nashorn erstellt wird.

Anschließend diskutierten wir die anderen Möglichkeiten zur Erweiterung der Front-End-Anwendung und die Vorteile der Verwendung einer isomorphen App.

Wie immer ist der Code auf GitHub zu finden.