Developing Javascript frontends with Webpack

This post assumes that you are at least familiar with Webpack. If not, you might find this post helpful. It explains first why we stopped developing portlets and use a javascript frontend + rest services approach. It doesn’t cover the rest backend at all. Then it explains why we use Webpack as a task runner and compare it a bit to the Liferay NPM Bundler.

A little and simple demo application is presented afterwards. It shows some basic use of Web Components and how to lazy load applications using javascript on demand.

Motivation

Let me start with a few little preamble to explain our approach to developing applications.

The days when websites were static entities and all the magic happened on the server are long gone. Nowadays Javascript is an integral and huge part of the web. Customers expect and demand more than ever before, the web applications of today have to be as powerful as desktop applications, with excellent usability on phone and desktop. Click and wait is a no-go.

Developers and web designers are challenged to meet and exceed expectations. We need to develop faster, more beautiful applications, with better usability, WCAG compliant, secure and of course: With smaller budgets.

For those reasons, my team and I decided several years ago that we needed to change how we worked. While we cannot fulfill all these requirements all the time (especially the budget one), we still saw room for improvement.

Rest backend and Javascript frontend

We decided to modularize the backend and to use mainly rest service endpoints. Kinda the microservice approach, but with Liferay as a platform. We fiddled a bit with that in Liferay 6.2 and had a hard time with it, with 7.0 it became easy and with 7.2 implementing the Whiteboard specification it became a blast.

With that came the need to use modern frontend libraries/frameworks in the browser. To do that you need to have a build and development process that supports that. We decided to use Webpack as our main build tool and module bundler. We use it to transpile our ECMAScript code to support Internet Explorer 11. The scss files are compiled, autoprefixed and minified.

After some consideration we decided to use Vue.js for our components, but we are not religious about it.

We mostly have stopped writing portlets at all and when we do, they are usually just shells adding some html to a page. The most useful thing about portlets is the configuration page.

We found that Web content and Freemarker templates are very convenient. Web content structures allow us to add some content, like headline and description and even some configuration.  The templates write the necessary html tags to the page, the javascript application renders itself into them and often uses rest calls to fetch further data. With that, any Webcontent can become an application.

With 7.2 we plan to replace that with the new Fragments (awesome new feature!), but we are not there yet.

Webpack vs. the Liferay NPM Bundler

While this blog post covers the Webpack approach, there is an alternative. Liferay has created their own npm bundler and improved it last year. It does things a bit differently, but it is a worthwhile alternative and quite impressive.

I’d like to outline why we still prefer to bundle our stuff using Webpack.

Webpack is currently the most successful and popular frontend module bundler and for good reason. It has lots of users, a ton of documentation and also lots of contributors who improve it all the time. It is also used in other departments in our company. If you are stuck, chances are high that you find a solution.

The development proxy is a real gem that allows hot reloading of changes. Instead of writing code and deploying it, you just edit the code/scss and when you save, the page changes instantly. For those who have never seen this, here is a small animation showing the effect.

I just save and the content in the browser changes an instant later. Whenever a file in the source folder is changed, the relevant file is transpiled and the change is transmitted to the browser. This really speeds up development. If necessary we can even proxy the productive system and debug  issues there. This can be quite useful when an issue can only be seen in production.

Using the development proxy also has the advantage that the frontend developer only needs to have limited knowledge of Liferay. He starts Liferay, then starts the dev server and starts working on the frontend. He doesn’t need to know about portlet actions and all those pesky details a portlet brings with it.

The frontend code is actually quite backend agnostic.It depends on the theme styling and usually a few other things, but in general, it would also work with a few changes on any html page.

But there is also one disadvantage of our approach.

Webpack works best when it processes all the javascript code of a webpage. Then it can optimize the result and discard everything that is not needed/used (a process called tree shaking).

This basically leads to one limitation: All frontend code needs to be compiled in a single Webpack build. While it would be possible to work around this limitation to some degree, it is really how Webpack is supposed to be used. It’s part of the concept.

The simplest solution we found was to put (nearly) all our javascript and css into a single frontend loader project. We pondered some other schemes but adding a subfolder per application works quite well so far.

A change to the application usually means to deploy the relevant rest service(s) and to deploy the whole frontend module. The rest services are individual modules and can be deployed extra, but the frontend is one package. So far, this wasn’t a big deal for us. But maybe you feel different.

The Liferay npm bundler takes a different approach and does better here.

It allows to put the javascript code into distinct modules. The modules are more independent than in our approach. Obviously, since the modules are all independent, it can’t optimize that well. But that’s a minor thing.

The main downside here is that it works only in Liferay, it is not widely used, so there isn’t  a ton of information out there. In general, Webpack has more features, most notably hot reloading as shown above.

We like Webpack, but maybe you like the Liferay approach better.

An example application

Now, let’s dive into it.

I have prepared a little example project on github. I have tested it with Liferay 7.2 and Firefox/Chrome. It should work with all browsers and Internet Explorer 11, but since I didn’t test it, I can’t guarantee it.

The example assumes that you have three pages each containing a simple application:

  • Hello World

  • Today

  • Shuffled Words

The idea here is to show how multiple applications could be put into a single build and dynamically loaded on demand. Only code that is used/needed, should be loaded. Some code needs to be loaded at once, but it is pretty small. If we didn’t need to support Internet Explorer, the generated javascript code would be even smaller (The difference in the example is about 120 KB).

To test the example, you can do it online here. Please note that this page is based on Liferay 7.0. As you can see, the javascript code is pretty independent of the Liferay version.

If you don’t want to build it yourself and just try it, you can download the module here. Please note that it was built for Liferay 7.2 and won’t deploy in other versions.

If you want to build it yourself, please clone the repository, build the frontend-loader module and deploy it in Liferay 7.2. Please read the build instructions in the Readme. To make it work in other 7.x version, you need to change the dependency version in the build.gradle file

Whether you deploy the jar file or build it yourself, you need to use the following steps to test the code.

  1. Create three pages: Hello World, Today and Words.

  2. Create three webcontents with the following content

  3. Put them on the respective pages.

Attention: You need to switch to the source code view! Otherwise it will be added as text and not as an html tag.

Hello World

<hello-world></hello-world>


Today

<app-today></app-today>


Shuffled Words

<app-shuffle words='Liferay is simply great!'></app-shuffle>


Just an html tag. That’s all. Ok, shuffle expects a list of words as “configuration”, but that’s actually to show how you would add configuration to such a tag.

Now you should see “Hello World!” on the first page, what day it is on the second and “Liferay is simply great!” shuffled on the third.

If you open the developer tools in the browser and monitor the network traffic on the pages, you will notice that lodash and date-fns are not loaded on the “Hello World” page. They are only loaded on the pages that need them. Please note that some <number>.js file is loaded too. This is the actual component with its dependencies. Webpack automatically names the dynamic imports and choses a number for them.

Anyway: The really important thing is that everything is loaded on demand. This makes pages really fast.

app.js

App.js is the entry point of the application. In it we define one Webcomponent per application. When a fitting html tag (e.g. <hello-world>) “appears” on the page (is added to the DOM), the connectedCallback method is executed. In that method we simply call the import function of Webpack and it loads our application and all of its dependencies.

Today depends on the date-fns library, Shuffled Words depends on the lodash library. Those libraries are only loaded when the page contains the respective html tags.

// We create a Webcomponent called today
class Today extends HTMLElement {
  connectedCallback() {
      // Lazy load the actual implementation
    import('./components/Today/today.js').then(module => {
      // Call the default method of our application
      module.default(this);
    }).catch(error => console.error('An error occurred while loading the component `Today`', error));
  }
}
// Now we link our component to the html tag <app-today>
register('app-today', Today);



Today.js

// We import date-fns to showcase that date-fns is loaded dynamically when we load the component.
import { format } from 'date-fns';

// We export the initialization function as default
// It expects a html element as parameter and simply inserts some text into it
export default function showToday(element) {
  const today = new Date();
  element.innerHTML = 'Today is a ' + format(today, 'dddd');
}


The package.json

The purpose of this file is similar to the build.gradle files. It contains a list of dependencies and several build targets. When you build the module for deployment, the “prod” task is executed. It optimizes and minifies the output for production environment.

Since the minified code is quite ghastly to read, there is also the option to do a development build and also to start the dev server by starting it “hot”. Using hot, a watchdog process is started that recompiles the relevant code on every save. A dev server is started on port 3000 and it proxies all requests, except for our application, to the port 8080 where Liferay runs.

If you have built, deployed and configured it yourself, you can try it out now yourself. Open a command line window, switch to the module folder “dccs-loader-demo” and type “npm run hot”.

If it doesn’t open a window by itself, go to “http://localhost:3000” in the browser. You should see your Liferay page since the dev server proxies it. It also intercepts requests for our js/css files and replaces them with development versions.

Now, open “hello.js” in any browser, change the text and save. It should behave as in the video above. With this method, developing a javascript application becomes really fun. Compare it to “blade gw deploy” and you will certainly notice the difference ...

The Webpack config

While it might feel a bit overwhelming if you have never used it before, it isn’t too complicated. Webpack expects a json with lots of configuration options as a source. If you don’t like the defaults, you need to overwrite them. There is a ton of resources on the Internet describing how such a config works.

I’d like to mention just a few specialities. First of all, with Webpack 4 the CommonsChunkPlugin was replaced by the SplitChunks plugin. It’s more powerful. Uglify is gone too, TerserPlugin is the new kid on the block. If you search for resources on the internet, you will find configs for Webpack 1,2,3. Be careful, some of the configurations might not work anymore or work differently in Webpack 4.

I have added quite a few more comments than we normally add, I hope they help you understand the settings and the intention behind them.