Applicazione isomorfa con React e Nashorn

1. Panoramica

In questo tutorial, capiremo cos'è esattamente un'app isomorfa. Discuteremo anche di Nashorn, il motore JavaScript in bundle con Java.

Inoltre, esploreremo come possiamo utilizzare Nashorn insieme a una libreria front-end come React per creare un'app isomorfa.

2. Un po 'di storia

Tradizionalmente, le applicazioni client e server venivano scritte in un modo piuttosto pesante sul lato server. Pensa a PHP come a un motore di scripting che genera per lo più HTML statico e browser web che li riproducono.

Netscape è arrivato con il supporto di JavaScript nel suo browser alla metà degli anni novanta . Ciò ha iniziato a spostare parte dell'elaborazione dal lato server al browser lato client. Per molto tempo, gli sviluppatori hanno lottato con diversi problemi relativi al supporto JavaScript nei browser web.

Con la crescente domanda di un'esperienza utente più veloce e interattiva, il confine era già stato spinto più forte. Uno dei primi framework che ha cambiato il gioco è stato jQuery. Ha portato diverse funzioni user-friendly e un supporto molto migliorato per AJAX.

Presto iniziarono ad apparire molti framework per lo sviluppo front-end, che migliorarono notevolmente l'esperienza dello sviluppatore. A partire da AngularJS di Google, React da Facebook e, successivamente, Vue, hanno iniziato a catturare l'attenzione degli sviluppatori.

Con il supporto del browser moderno, framework straordinari e strumenti necessari, le maree si stanno spostando in gran parte verso il lato client .

Un'esperienza coinvolgente su dispositivi portatili sempre più veloci richiede più elaborazione lato client.

3. Che cos'è un'app isomorfa?

Quindi, abbiamo visto come i framework front-end ci stanno aiutando a sviluppare un'applicazione web in cui l'interfaccia utente è completamente renderizzata sul lato client.

Tuttavia, è anche possibile utilizzare lo stesso framework sul lato server e generare la stessa interfaccia utente.

Ora, non dobbiamo necessariamente attenerci alle soluzioni solo lato client o solo lato server. Un modo migliore è disporre di una soluzione in cui il client e il server possano elaborare lo stesso codice front-end e generare la stessa interfaccia utente.

Ci sono vantaggi in questo approccio, di cui parleremo più avanti.

Tali applicazioni web sono chiamate isomorfiche o universali . Ora il linguaggio lato client è più esclusivamente JavaScript. Quindi, affinché un'app isomorfa funzioni, dobbiamo utilizzare JavaScript anche sul lato server.

Node.js è di gran lunga la scelta più comune per creare un'applicazione renderizzata lato server.

4. Che cos'è Nashorn?

Allora, dove si inserisce Nashorn e perché dovremmo usarlo? Nashorn è un motore JavaScript impacchettato per impostazione predefinita con Java . Quindi, se abbiamo già un back-end per applicazioni web in Java e vogliamo creare un'app isomorfa, Nashorn è piuttosto utile!

Nashorn è stato rilasciato come parte di Java 8. Questo è principalmente focalizzato sull'autorizzazione di applicazioni JavaScript incorporate in Java.

Nashorn compila JavaScript in memoria in Java Bytecode e lo passa alla JVM per l'esecuzione. Questo offre prestazioni migliori rispetto al motore precedente, Rhino.

5. Creazione di un'app isomorfa

Abbiamo esaminato abbastanza contesto ora. La nostra applicazione qui mostrerà una sequenza di Fibonacci e fornirà un pulsante per generare e visualizzare il numero successivo nella sequenza. Creiamo ora una semplice app isomorfa con un back-end e un front-end:

  • Front-end: un semplice front-end basato su React.js
  • Back-end: un semplice back-end Spring Boot con Nashorn per elaborare JavaScript

6. Front-end dell'applicazione

Useremo React.js per creare il nostro front-end . React è una popolare libreria JavaScript per la creazione di app a pagina singola. Ci aiuta a scomporre un'interfaccia utente complessa in componenti gerarchici con stato opzionale e associazione dati unidirezionale.

React analizza questa gerarchia e crea una struttura di dati in memoria chiamata DOM virtuale. Questo aiuta React a trovare le modifiche tra i diversi stati e ad apportare modifiche minime al DOM del browser.

6.1. Componente di reazione

Creiamo il nostro primo componente 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}) ) ); } });

Ora, capiamo cosa sta facendo il codice sopra:

  • Per cominciare, abbiamo definito un componente di classe in React chiamato "App"
  • La funzione più importante all'interno di questo componente è "render" , che è responsabile della generazione dell'interfaccia utente
  • Abbiamo fornito uno stile className che il componente può utilizzare
  • Stiamo facendo uso dello stato dei componenti qui per memorizzare e visualizzare le serie
  • Mentre lo stato viene inizializzato come un elenco vuoto, recupera i dati passati al componente come prop quando il componente viene montato
  • Infine, al clic del pulsante "Aggiungi", viene effettuata una chiamata jQuery al servizio REST
  • La chiamata recupera il numero successivo nella sequenza e lo aggiunge allo stato del componente
  • La modifica dello stato del componente esegue automaticamente il rendering del componente

6.2. Utilizzo del componente React

React cerca un elemento denominato "div" nella pagina HTML per ancorare i suoi contenuti . Tutto quello che dobbiamo fare è fornire una pagina HTML con questo elemento "div" e caricare i file JS:

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

Quindi, vediamo cosa abbiamo fatto qui:

  • Abbiamo importato le librerie JS richieste, React, React-Dom e jQuery
  • Successivamente, abbiamo definito un elemento "div" chiamato "root"
  • Abbiamo anche importato il file JS con il nostro componente React
  • Successivamente, abbiamo chiamato il componente React "App" con alcuni dati seed, i primi tre numeri di Fibonacci

7. Back-end dell'applicazione

Vediamo ora come creare un back-end adatto per la nostra applicazione. Abbiamo già deciso di utilizzare Spring Boot insieme a Spring Web per creare questa applicazione. Ancora più importante, abbiamo deciso di utilizzare Nashorn per elaborare il front-end basato su JavaScript che abbiamo sviluppato nell'ultima sezione.

7.1. Dipendenze di Maven

Per la nostra semplice applicazione, utilizzeremo JSP insieme a Spring MVC, quindi aggiungeremo un paio di dipendenze al nostro POM:

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

La prima è la dipendenza standard per l'avvio a molla per un'applicazione web. Il secondo è necessario per compilare JSP.

7.2. Controller web

Creiamo ora il nostro controller web, che elaborerà il nostro file JavaScript e restituirà un HTML utilizzando 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"; } }

Quindi, cosa sta succedendo esattamente qui:

  • Recuperiamo un'istanza di ScriptEngine di tipo Nashorn da ScriptEngineManager
  • Quindi, carichiamo le librerie pertinenti in React, react.js e react-dom-server.js
  • Carichiamo anche il nostro file JS che ha il nostro componente di reazione "App"
  • Infine, valutiamo un frammento JS creando un elemento React con il componente “App” e alcuni dati seed
  • Questo ci fornisce un output di React, un frammento HTML come Object
  • Passiamo questo frammento HTML come dati alla vista pertinente: il 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. Conclusione

In questo tutorial, abbiamo esaminato i concetti di base delle applicazioni isomorfe e del motore JavaScript Nashorn. Abbiamo ulteriormente esplorato come creare un'app isomorfa con Spring Boot, React e Nashorn.

Quindi, abbiamo discusso le altre possibilità per estendere l'applicazione front-end e i vantaggi dell'utilizzo di un'app isomorfa.

Come sempre, il codice può essere trovato su GitHub.