Application isomorphe avec React et Nashorn

1. Vue d'ensemble

Dans ce didacticiel, nous allons comprendre ce qu'est exactement une application isomorphe. Nous discuterons également de Nashorn, le moteur JavaScript fourni avec Java.

De plus, nous explorerons comment nous pouvons utiliser Nashorn avec une bibliothèque frontale comme React pour créer une application isomorphe.

2. Un peu d'histoire

Traditionnellement, les applications client et serveur étaient écrites d'une manière assez lourde côté serveur. Considérez PHP comme un moteur de script générant principalement du HTML statique et des navigateurs Web les rendant.

Netscape est venu avec le support de JavaScript dans son navigateur au milieu des années 90 . Cela a commencé à déplacer une partie du traitement du côté serveur vers le navigateur côté client. Pendant longtemps, les développeurs ont été confrontés à différents problèmes concernant la prise en charge de JavaScript dans les navigateurs Web.

Avec la demande croissante pour une expérience utilisateur plus rapide et interactive, la limite était déjà repoussée. L'un des premiers frameworks qui a changé le jeu était jQuery. Il a apporté plusieurs fonctions conviviales et un support bien amélioré pour AJAX.

Bientôt, de nombreux frameworks pour le développement front-end ont commencé à apparaître, ce qui a considérablement amélioré l'expérience du développeur. En commençant par AngularJS de Google, React de Facebook et plus tard, Vue, ils ont commencé à capter l'attention des développeurs.

Avec la prise en charge des navigateurs modernes, des cadres remarquables et des outils requis, les tendances se déplacent largement vers le côté client .

Une expérience immersive sur des appareils portables de plus en plus rapides nécessite davantage de traitement côté client.

3. Qu'est-ce qu'une application isomorphe?

Nous avons donc vu comment les frameworks frontaux nous aident à développer une application Web dans laquelle l'interface utilisateur est complètement rendue côté client.

Cependant, il est également possible d'utiliser le même framework côté serveur et de générer la même interface utilisateur.

Désormais, nous ne devons pas nécessairement nous en tenir aux solutions côté client uniquement ou côté serveur uniquement. Une meilleure façon est d'avoir une solution où le client et le serveur peuvent à la fois traiter le même code frontal et générer la même interface utilisateur.

Cette approche présente des avantages, dont nous parlerons plus tard.

Ces applications Web sont appelées Isomorphic ou Universal . Désormais, le langage côté client est le plus exclusivement JavaScript. Par conséquent, pour qu'une application isomorphe fonctionne, nous devons également utiliser JavaScript côté serveur.

Node.js est de loin le choix le plus courant pour créer une application rendue côté serveur.

4. Qu'est-ce que Nashorn?

Alors, où se situe Nashorn et pourquoi devrions-nous l'utiliser? Nashorn est un moteur JavaScript fourni par défaut avec Java . Par conséquent, si nous avons déjà un back-end d'application Web en Java et que nous voulons créer une application isomorphe, Nashorn est très pratique!

Nashorn a été publié dans le cadre de Java 8. Cela vise principalement à autoriser les applications JavaScript intégrées dans Java.

Nashorn compile JavaScript en mémoire en Java Bytecode et le transmet à la JVM pour exécution. Cela offre de meilleures performances par rapport au moteur précédent, Rhino.

5. Création d'une application isomorphe

Nous avons traversé suffisamment de contexte maintenant. Notre application affichera ici une séquence de Fibonacci et fournira un bouton pour générer et afficher le numéro suivant dans la séquence. Créons maintenant une application isomorphe simple avec un back-end et un front-end:

  • Front-end: Un simple front-end basé sur React.js
  • Back-end: Un simple back-end Spring Boot avec Nashorn pour traiter JavaScript

6. Application frontale

Nous utiliserons React.js pour créer notre frontal . React est une bibliothèque JavaScript populaire pour la création d'applications d'une seule page. Cela nous aide à décomposer une interface utilisateur complexe en composants hiérarchiques avec un état facultatif et une liaison de données unidirectionnelle.

React analyse cette hiérarchie et crée une structure de données en mémoire appelée DOM virtuel. Cela aide React à trouver les changements entre différents états et à apporter des modifications minimes au DOM du navigateur.

6.1. Composant React

Créons notre premier composant 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}) ) ); } });

Maintenant, comprenons ce que fait le code ci-dessus:

  • Pour commencer, nous avons défini un composant de classe dans React appelé «App»
  • La fonction la plus importante de ce composant est le «rendu» , qui est responsable de la génération de l'interface utilisateur
  • Nous avons fourni un style className que le composant peut utiliser
  • Nous utilisons ici l'état du composant pour stocker et afficher la série
  • Alors que l'état s'initialise en tant que liste vide, il récupère les données passées au composant en tant que accessoire lors du montage du composant
  • Enfin, en cliquant sur le bouton «Ajouter», un appel jQuery au service REST est effectué
  • L'appel récupère le numéro suivant dans la séquence et l'ajoute à l'état du composant
  • La modification de l'état du composant rend automatiquement le composant

6.2. Utilisation du composant React

React recherche un élément nommé «div» dans la page HTML pour ancrer son contenu . Tout ce que nous avons à faire est de fournir une page HTML avec cet élément «div» et de charger les fichiers JS:

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

Alors, voyons ce que nous avons fait ici:

  • 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. Conclusion

Dans ce tutoriel, nous avons passé en revue les concepts de base des applications isomorphes et du moteur JavaScript Nashorn. Nous avons exploré plus en détail comment créer une application isomorphe avec Spring Boot, React et Nashorn.

Ensuite, nous avons discuté des autres possibilités d'étendre l'application frontale et des avantages de l'utilisation d'une application isomorphe.

Comme toujours, le code peut être trouvé sur GitHub.