📜 ⬆️ ⬇️

Isomorphic React JS + Spring Boot application



About ReactJs, Java, Spring, rendering, Virtual DOM , Redux and other such things there are already a lot of various articles and practical developments, so I will not go into them.

I did not measure the performance of this design. Those who are interested can conduct their own personal tests and compare, for example, with NodeJS .
')
I didn’t really bother with the style and quality of the code, so excuse me who doesn’t like it =)

The goal of my work is just to make things like ReactJS + Redux + WebPack + Java / Spring work together.

Before writing my article, I used the search and found an existing similar example . But, after reading the comments, I noticed that some people want to see a similar sample of an isomorphic application, but working with Spring .

I am happy to fulfill the desire of readers.

The architecture of my example will contain the following components:

FrontEnd:


BackEnd:


Yes, I almost forgot - during the demonstration of the work of the UI-part I used the open source library arui-feather , which was kindly provided by colleagues from Alpha Laboratories . The library contains a lot of various UI-components with a pre-installed work logic.

So, what is the whole thing to turn? And honestly - on what you want. Tomcat EE, JBoss, WildFly. I personally use WildFly.

Advantages of this approach:


Part One - Boilerplate Generation


So, let's start - go to http://start.spring.io , fill in the form for the generation of the boiler plate, as in the screenshot.



Of the required dependencies, we need the Web to create the backends and the HTML template engine Thymeleaf . Library Lombok can be delivered at will.

Then I click on the Generate Project and download the zip-archive with the boilerplate. Then I unpack the archive into free disk space and import the boilerplate into the Intellij IDEA development environment .

Part two - project structure


The whole project, as it is easy to guess, will consist of two parts: Frontend (folder of the same name) and Backend (src / main / java).

The structure of the front part is shown in the picture below:



I have everything that is needed for the work of ReactJS + Redux + WebPack.

Let's sort everything out in order:

index.jsx is the entry point to our application:

import React from 'react' import { render } from 'react-dom' import { renderToString } from 'react-dom/server' import thunk from 'redux-thunk' import { Provider } from 'react-redux' import { createStore, applyMiddleware } from 'redux' import testmiddleware from './middlewares/testmiddleware' import promise from 'redux-promise' import reduxReset from 'redux-reset' import reducers from './reducers/reducers' import App from './components/app/app' const MIDDLEWARES = [ thunk, promise, testmiddleware ]; if (typeof window !== 'undefined' && typeof document !== 'undefined' && typeof document.createElement === 'function') { window.renderClient = (state) => { let store = applyMiddleware(...MIDDLEWARES)(createStore)(reducers, state, reduxReset()); store.subscribe(() => console.log(store.getState())); render ( <Provider store={ store }> <App /> </Provider>, document.getElementById ('root') ); } } else { global.renderServer = (state) => { let store = applyMiddleware(...MIDDLEWARES)(createStore)(reducers, state, reduxReset()); store.subscribe(() => console.log(store.getState())); return renderToString ( <Provider store={ store }> <App /> </Provider> ) } } 

It contains two main functions - renderClient () and renderServer () .

The renderClient () function will be responsible for the logic of the front part, after our isomorphic application is fully rendered by the server. To make this happen, first renderServer () ;

app.jsx - the root component that will be responsible for mounting and displaying other components.

 import React from 'react' import AppTitle from 'arui-feather/app-title' import AppContent from 'arui-feather/app-content' import AuthForm from '../authform/authform' import Footer from 'arui-feather/footer' import Header from 'arui-feather/header' import Heading from 'arui-feather/heading' import Page from 'arui-feather/page' class App extends React.Component { render() { return ( <Page header={ <Header />} footer={<Footer />} > <AppTitle> <Heading> </Heading> </AppTitle> <AppContent> <AuthForm/> </AppContent> </Page> ); } } export default App; 

authform.jsx is a component of the form with which we will send requests to our server.

 import React from 'react' import { bindActionCreators } from 'redux' import {Field, formValueSelector, reduxForm } from 'redux-form' import { connect } from 'react-redux' import { makeTestAction } from '../../actions/testaction' import Button from 'arui-feather/button' import Form from 'arui-feather/form' import FormField from 'arui-feather/form-field' import Input from 'arui-feather/input' import Label from 'arui-feather/label' import { inputField } from '../../utils/componentFactory' let formConfig = { form: 'testForm' }; let foundStatus = ""; const selector = formValueSelector('testForm'); function mapStateToProps(state) { return { phoneField: selector(state, 'phoneNumber'), statusResp: state.testRed.respResult, answer: state.testRed.answerReceived }; } function mapDispatchToProps(dispatch) { return bindActionCreators( { makeTestAction }, dispatch ) } @reduxForm(formConfig) @connect(mapStateToProps, mapDispatchToProps) class AuthForm extends React.Component { render() { return ( <div> <Form noValidate={ true } onSubmit={ this.props.makeTestAction }> <FormField key='phoneNumber'> <Field name='phoneNumber' placeholder='     79001234567' component={ inputField } size='m' /> </FormField> <FormField view='line'> <Button width='available' view='extra' size='m' type='submit'>  </Button> </FormField> { this.renderFinalResult() } </Form> </div> ); } renderFinalResult() { foundStatus = this.props.statusResp; return (this.props.answer === true ) && <div> <FormField view='line' width='400px' label={ <Label size='m'>  </Label> }> <Input size='m' width='available' value={ foundStatus } /> </FormField> </div> } } export default AuthForm; 

When we initiate the form submission onSubmit = {this.props.makeTestAction} , we create a request to execute the action (dispatch) called makeTestAction .

testaction.js:

 import { TEST_ACTION, TEST_START, TEST_SUCCESS, TEST_FAILRULE } from '../constants/actions'; export function makeTestAction() { return { type: TEST_ACTION, actions: [ TEST_START, TEST_SUCCESS, TEST_FAILRULE ] } } 

Next comes the middleware component. Our testmiddleware is sharpened to respond each time TEST_ACTION is initiated.

testmiddleware.js:

 import { TEST_ACTION } from '../constants/actions' import superagent from 'superagent' const testmiddleware = store => next => action => { if (action.type !== TEST_ACTION) { return next(action); } const [ startAction, successAction, failureAction] = action.actions; const fieldName = action.fieldName; let state = store.getState(); let dataFetch = state.form.testForm.values; if (!action.value) { store.dispatch({ type: successAction, fieldName, payload:[] }); } store.dispatch({ type: startAction, fieldName }); superagent .get('/testep') .set('Content-Type', 'text/html; charset=utf-8') .query(dataFetch) .timeout(10000) .end((error, res) => { if (!error && res.ok) { store.dispatch({ type: successAction, fieldName, payload: JSON.parse(res.text) }); } else { console.log("ERROR!!!"); } }); return 1; }; export default testmiddleware; 

Here I use the add-on superagent to send a get-request to the endpoint “/ testep” and, if the request finds an endpoint and an answer comes, then I put the answer in our store as a variable payload and initiate successAction .

testreducer.js - our only reducer is at the ready and waiting for successAction to be finally initiated:

 import { TEST_SUCCESS } from '../constants/actions' let initialState = {}; export default function testReducer(state = initialState, action) { if (action.type === TEST_SUCCESS) { return { ...state, respResult: action.payload.name, answerReceived: true }; } return { ...state, answerReceived: false } } 

As soon as this happens, the result of our payload is recorded in the store and given to our UI in the form of a statusResp variable.

That is, in fact, all that concerns the work of our frontend.

As for the backend, then everything is much simpler. We will have the most ordinary REST service, according to the most standard scheme:



The most interesting for us here will be the file React.java

 package ru.alfabank.ef.configurations; import jdk.nashorn.api.scripting.NashornScriptEngine; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.io.IOException; @Component public class React { @Value(value = "classpath:static/nashorn-polyfill.js") private Resource nashornPolyfillFile; @Value(value = "classpath:static/bundle.js") private Resource bundleJsFile; public String renderEntryPoint() throws ScriptException, IOException { NashornScriptEngine nashornScriptEngine = getNashornScriptEngine(); try { Object html = nashornScriptEngine.invokeFunction("renderServer"); return String.valueOf(html); } catch (Exception e) { throw new IllegalStateException("Error! Failed to render react component!", e); } } private NashornScriptEngine getNashornScriptEngine() throws ScriptException, IOException { NashornScriptEngine nashornScriptEngine = (NashornScriptEngine) new ScriptEngineManager().getEngineByName ("nashorn"); nashornScriptEngine.eval ("load ('" + nashornPolyfillFile.getFile().getCanonicalPath() + "')"); nashornScriptEngine.eval ("load ('" + bundleJsFile.getFile().getCanonicalPath() + "')"); return nashornScriptEngine; } } 

This file is responsible for reading the bundle.js file , which is a compressed warehouse for all our scripts and UI components, as well as for calling the renderServer () function, which initiates the rendering of our application on the server once before the logic of our application will be continue working on the client by triggering the renderClient () function (see the index.jsx file).

Also our file returns html-page . As soon as the main controller runs:

 package ru.alfabank.ef.controllers; import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import ru.alfabank.ef.configurations.React; import javax.script.ScriptException; import java.io.FileNotFoundException; @Controller public class MainController { private final React react; @Autowired public MainController(React react) { this.react = react; } @RequestMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE) public String mainPage(Model model) throws JsonProcessingException, ScriptException, FileNotFoundException { String renderedHTML = react.renderEntryPoint(); model.addAttribute("content", renderedHTML); return "index"; } } 

The model object will be embedded into the index.html page (template) through the content attribute in order to work on the server before starting to work on the client.

Template index.html :

 <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>SpringBoot & React | Progressive Webapp Demo</title> <link rel="stylesheet" href="/styles.css" /> </head> <body> <div id="root" th:utext="${content}"></div> <script src="/bundle.js"></script> <script th:inline="javascript"> window.renderClient(); </script> </body> </html> 

Part Three - Build and Run


Clone our repository, recruit a team

mvn clean install

We are waiting for our project to be assembled, and then we throw the finished test.war file on JBoss, WildFly or Tomcat EE.

After the artifact is successfully closed, open the browser and type localhost : 8080. After just a couple of seconds of loading, a test example opens.

On the screen fragment of the form:



All the rest of our backend content is a typical spring REST service using a standard template.

When you enter the number in the correct format, the answer will be returned - OK.
If the number is incorrect, you will receive ERROR from our REST service.

That's all.

The plans for the future - to transfer the whole thing to the gradle, fasten a hot reboot of the front, docker-container well, and of course cover the whole thing with tests.

All files of interest can be downloaded from the repository link:

bitbucket.org/serpentcross/alfabank-ef-test

The materials that I used when developing the example:

- ARUI-Feather library: alfa-laboratory.imtqy.com/arui-feather/styleguide
- Redux Form: redux.js.org
- Creating isomorphic applications: winterbe.com/posts/2015/02/16/isomorphic-react-webapps-on-the-jvm
- Spring Project Generator: start.spring.io
- Example of an isomorphous application: github.com/synyx/springboot-reactjs-demo

Thanks to all!!!

Source: https://habr.com/ru/post/340776/


All Articles