A Java programmer’s guide to delivering webapp frontends

When I started investigating (googling) web frontends, I found tutorials for various frameworks and I found tutorials using node.js to deliver frontends to web browsers on localhost.

What I did not find, was something telling me how I should pack up and deliver the webapp frontend from my production system, so that comes here:

  1. A web application frontend  is a piece of javascript code, that builds its user interface by rebuilding the DOM tree of the webpage that loaded it
  2. The javascript needed for a webapp is packaged up as a big javascript file, typically named bundle.js,
  3. The initial HTML file references bundle.js
  4. The initial HTML file also contains an element that matches what the javascript code expects to be its start element (i.e. the point in the DOM of the parsed HTML file, where the javascript program starts rebuilding its own DOM)

Most react.js tutorials start with an example like this (this is the “main()” of the javascript program):

ReactDOM.render(
<App/>,
document.getElementById('root'));
view raw index.js hosted with ❤ by GitHub

The above code example tries to find the element with id root in the DOM of the surrounding HTML file, and replacing it with the DOM of the <App/> react component (i.e. running the render() method of the component).

This means that for anything to appear in the HTML page there must be an element in the HTML page with id “root”.  If not, react won’t find a place to start rewriting the DOM and won’t do anything.

A minimal HTML page loading bundle.js and with an element where javascript can start rebuilding the DOM, looks like this:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Frontend Karaf Test</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
</head>
<body>
<div id="root"></div>
<script src="bundle.js" type="application/javascript"></script
<noscript>This webpage requires javascript in the browser!</noscript>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

This page will load the bundle.js and contains the element <div id=”root” /> where react can start rewriting the DOM.

It is possible to serve this two part web application as static files, i.e. dropping the index.html and bundle.js files into a directory served by nginx or apache will be sufficient to make the webapp load and start rendering in a web browser.

The frontend people and most frontend tutorials use node.js to build the bundle.js and serve the index.html and the bundle.js. That’s fine for developing the actual frontend application, but not for delivering the application to the production enviroment.

Working in java my first thought was “how do I package up the frontend in a jar file that can be deployed to a maven repository?”. The rest of this article attempts to answer this question.

  1. The frontend-maven-plugin is used to build a bundle.js from the src/main/frontend/ directory of a project
    <plugin>
    <groupId>com.github.eirslett</groupId>
    <artifactId>frontend-maven-plugin</artifactId>
    <version>1.6</version>
    <configuration>
    <nodeVersion>v10.4.0</nodeVersion>
    <workingDirectory>src/main/frontend</workingDirectory>
    <installDirectory>target</installDirectory>
    </configuration>
    <executions>
    <execution>
    <id>install node and npm</id>
    <goals>
    <goal>install-node-and-npm</goal>
    </goals>
    </execution>
    <execution>
    <id>npm install</id>
    <goals>
    <goal>npm</goal>
    </goals>
    </execution>
    <execution>
    <id>webpack build</id>
    <goals>
    <goal>webpack</goal>
    </goals>
    </execution>
    </executions>
    </plugin>
    view raw pom.xml hosted with ❤ by GitHub
  2. The bundle.js created by frontend-maven-plugin is included as a resource in the jar (handled by having frontend-maven-plugin drop the bundle.js in the target/classes/ directory)
  3. The index.html file is put into src/main/resources/ and end up as a resource in the jar
  4. A servlet is added to the code.  The servlet will return “index.html” for the request path “/” and otherwise look through the classpath for a match
    public class ReactServlet extends HttpServlet {
    private static final long serialVersionUID = 250817058831319271L;
    // The paths used to serve index.html
    private final List<String> routes = Arrays.asList("/", "/counter", "/about");
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
    String pathInfo = request.getPathInfo();
    try {
    if (pathInfo == null) {
    // Browsers won't redirect to bundle.js if the servlet path doesn't end with a "/"
    addSlashToServletPath(request, response);
    return;
    }
    String resource = findResourceFromPathInfo(pathInfo);
    String contentType = guessContentTypeFromResourceName(resource);
    response.setContentType(contentType);
    try(ServletOutputStream responseBody = response.getOutputStream()) {
    try(InputStream resourceFromClasspath = getClass().getClassLoader().getResourceAsStream(resource)) {
    if (resourceFromClasspath != null) {
    copyStream(resourceFromClasspath, responseBody);
    response.setStatus(200);
    return;
    }
    String message = String.format("Resource \"%s\" not found on the classpath", resource);
    response.sendError(404, message);
    }
    }
    } catch (IOException e) {
    response.setStatus(500); // Report internal server error
    }
    }
    String guessContentTypeFromResourceName(String resource) {
    String contentType = URLConnection.guessContentTypeFromName(resource);
    if (contentType != null) {
    return contentType;
    }
    String extension = resource.substring(resource.lastIndexOf('.') + 1);
    if ("xhtml".equals(extension)) {
    return "text/html";
    }
    if ("js".equals(extension)) {
    return "application/javascript";
    }
    if ("css".equals(extension)) {
    return "text/css";
    }
    return null;
    }
    private String findResourceFromPathInfo(String pathInfo) {
    if (routes.contains(pathInfo)) {
    return "index.xhtml";
    }
    return pathInfo;
    }
    private void addSlashToServletPath(HttpServletRequest request, HttpServletResponse response) throws IOException {
    response.sendRedirect(String.format("%s/", request.getServletPath()));
    }
    private void copyStream(InputStream input, ServletOutputStream output) throws IOException {
    int c;
    while((c = input.read()) != -1) {
    output.write(c);
    }
    }
    }

And that’s it, basically.

Edit: I got tired of copy-pasting the above code, so I packed it up in a base class and deployed it to maven central. It’s available under the Apache v2 license (see also Simplified delivery of react.js from apache karaf)  

You will need stuff to wire up your servlet to a path in the servlet container.   This could e.g. be a web-inf/web.xml deployment descriptor of a war file, or it could be annotations of servlet 3.0. Personally I use the OSGi web whiteboard: the servlet becomes a declarative services (DS) component that is started and plugs into a path (web context) in the servlet container.

Another thing to consider, is if you use the react-router or similar to rewrite the URL when moving to a different page, then you would like to enter the frontend application at all of the paths matching a page.

Some frontend examples (all using OSGi web whiteboard):

  1. The frontend-karaf-demo (a single maven module creating a declarative services component plugging into the web whiteboard on the webcontext “frontend-karaf-demo” and would become available at http://localhost:8181/frontend-karaf-demo/ when being loaded into apache karaf)
  2. The authservice user administration GUI
  3. The ukelonn weekly allowance web application

One thought on “A Java programmer’s guide to delivering webapp frontends”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.