2020-06-08 19:00:00 +0000

In the good old days, we implemented web applications with a server-side web framework. The browser sends a request to the server, the server processes the request and answers with HTML, the browser renders that HTML.

Nowadays, every application frontend seems to be a single page application (SPA) that loads a bunch of Javascript at the start and then uses Javascript-based templating to render the frontend.

What if we combine the two? This article shows a way of combining Vue.js components with a Thymeleaf-based server-side web application.

I used this method in blogtrack.io, a blog tracking service going into beta soon.

Code Example

This article is accompanied by a working code example on GitHub.

The Problems of SPAs

While SPAs allow for building more interactive, desktop-like applications, they also introduce new problems:

  • we need a mechanism to load only the Javascript resources we need on a certain page,
  • we might need to render part of the page on the server so that the user doesn’t see a blank page (time to first content),
  • we have to handle page refreshs and the back-button,
  • we have to handle analytics ourselves because analytics providers usually count when a page is loaded,
  • … and a whole bunch of other problems I don’t pretend to understand.

Solutions to many of these problems exist, but they add new problems (like the “time to interactive” metric) and complexity to the SPA frameworks, making them harder to use and understand. This leads to SPA fatigue in some people.

But building applications with only old-school server-side web frameworks is not a solution, either. We want modern, interactive frontends, for which we need Javascript.

So, what if we use a server-side web framework to render HTML that includes some Javascript components here and there, to add this interactivity?

Reusable Javascript Components

The goal is to create narrowly scoped, potentially re-usable Javascript components that we can place into the HTML rendered by our server-side web framework using <script> tags.

However, we don’t want to simply hack some untested Javascript that adds some JQuery here and there (it’s not the 90s anymore!) but take advantage of the rich feature set that today’s SPA frameworks bring to the table.

We want:

  • to preview the Javascript components without starting the server-side application,
  • to write and run tests for these Javascript components,
  • to include selected Javascript components in a server-rendered HTML page without loading all of them,
  • to minify the Javascript, and
  • to integrate the build of the Javascript components with the build of the server-side application.

Let’s see how we can achieve this by using client-side Vue.js components in HTML pages generated with the server-side templating engine Thymeleaf.

The Sample Project

For this article, imagine we’re building a dashboard application that displays some charts. We want to integrate the Chart.js library to create those charts. But instead of just adding hand-rolled, untested Javascript to our server-side HTML templates, we want to wrap those charts in components built with Vue.js.

We’re using server-generated HTML to render the layout and all the static and dynamic content that doesn’t require Javascript and only use Vue.js components for the interactive Javascript components.

In our project directory, we create a folder for the server-side Spring Boot application and another for the client-side Javascript components:

thymeleaf-vue
├── server
└── client

Let’s fill these folders with live!

Setting up the Server-Side Web Application with Spring Boot & Thymeleaf

We start by building a Spring Boot application that serves a page generated with the Thymeleaf templating engine.

We can let Spring Boot Initializr generate a ZIP file for us and extract the contents into the server folder (actually, we need to move the Gradle files back into the main folder - see the example project on Github for the final folder structure.

Next, we create the page template src/main/resources/templates/hello-vue.html:

<html>
  <body>
  <h1 th:text="${title}">This title will be replaced</h1>
  <p> Here comes a Vue component!</p>
  </body>
</html>

This is just a simple “Hello World”-style page that displays a title that is defined by the backend. We’re going to add a Vue.js component to it later.

Also, we add a controller that serves this page:

@Controller
class HelloVueController {

  @GetMapping("/hello")
  ModelAndView showHelloPage() {
  Map<String, Object> model = new HashMap<>();
  model.put("title", "Hello Vue!");
  return new ModelAndView("hello-vue.html", model);
  }

}

If we start the application with ./gradlew bootrun and go to http://localhost:8080/hello, we should see this page:

hello vue

We now have a working server-side web application driven by a Thymeleaf template. Time to create some Javascript components.

Building a Javascript Chart Component with Vue.js

For the client-side Javascript components, we’ll use Vue.js, which is a framework we can use to create SPAs, but which specifically supports exporting components to be consumed outside of a SPA.

We’ll need Node.js installed on our machine to support the Vue development environment.

When Node is installed, we can install the Vue CLI:

npm install -g @vue/cli

This brings us the vue command, which we use to create our Vue project. From the parent folder of our project (thymeleaf-vue), we run

vue create client

to create the client subfolder and fill it with a default Vue application. We end up with a file structure like this:

thymeleaf-vue
├── server
└── client
  ├── src
  |   ├── assets
  |   └── components
  └── package.json

I omitted some files for clarity.

Now, we want to create a Vue component that displays a chart. Let’s say the chart shall take 7 numbers as input, one for each day in the week, and display them in a bar chart.

First, we add the dependency to chart.js to our package.json file:

npm install --save chart.js

Next, we create our WeekChart component as a single file component:

<template>
  <div class="chart-container">
  <canvas ref="chart"></canvas>
  </div>
</template>

<script>
import Chart from "chart.js";

export default {
  name: "WeekChart",

  props: {
    chartData: {
      type: Array,
      required: true,
    },
  },

  mounted: function() {
    const config = {
      type: "bar",
      data: {
        labels: [
          "Monday", 
          "Tuesday", 
          "Wednesday",
          "Thursday",
          "Friday",
          "Saturday",
          "Sunday"],
        datasets: [
          {
          data: this.chartData
          },
        ]
      },
  };

  new Chart(this.$refs.chart, config);
  }
};
</script>

<style scoped>
.chart-container {
  position: relative;
  height: 100%;
  width: 100%;
}
</style>

This component bundles the HTML markup, some Javascript, and some CSS into a self-sufficient UI component. Note that we’re importing the Chart object from the chart.js library. The component has a single input parameter (or “prop” in JS speak) called chartData, which takes an array of values - one value for each day of the week.

Within the mounted function, we’re creating a chart configuration according to the chart.js docs, pass the chartData input parameter into this config, and finally bind this config to the <canvas> element in the template section via the ref=chart attribute.

In package.json, we change the build script so that it builds our component as a library instead of a SPA:

{
  ...
  "scripts": {
    ...
    "build": "vue-cli-service build --target lib --dest dist/WeekChart --name WeekChart src/components/WeekChart.vue",
  },
  ...
}

If we run npm run build now, the Vue CLI will create several different versions of our WeekChart component in the dist folder. The one we’re interested in is WeekChart.umd.min.js, which is a self-sufficient Javascript file containing all dependencies (except for Vue itself) that we can include in any HTML page.

Previewing the Vue Component with Storybook

Now that we’ve built a chart component, we want to see if it works without having to embed it into our application. For this, we’ll use Storybook.

Integrating Storybook with Vue is surprisingly simple, we merely have to execute this command in our client folder:

npx -p @storybook/cli sb init --type vue

This adds a storybook script and all required dependencies to our package.json and creates a folder stories, which now contains some sample “stories”.

We’ll add a story to the storybook by creating the file stories/WeekChart.stories.js with this content:

import WeekChart from '../src/components/WeekChart.vue';

export default {
  title: 'WeekChart',
  component: WeekChart,
};

export const DefaultState = () => ({
  components: { 
  chart: WeekChart
  },
  template: `<chart v-bind:chartData="[1,2,3,4,5,6,7]" />`
});

This file creates an instance of our WeekChart component with the name DefaultState and exports it so that Storybook can pick it up and include it in its GUI.

Running npm run storybook will start a local web server and serve the stories in a nice UI when we open http://localhost:6006 in a browser:

Storybook

We know that our bar chart component is working now. Storybook is nice to use during development to make sure that our changes have the desired effect. But if we do a refactoring to any of our components and forget to check it in Storybook, we may still break our components. So, let’s add an automated test that runs during the build.

Adding a Unit Test for the Vue Component

We want to create tests for each of our Vue components that run during the CI build to make sure that errors in a component will break the build. For this, we rely on Jest, a popular Javascript testing framework that integrates well with Vue.js.

To set up Jest in our project, we add the following entries to our package.json file:

{
  ...
  "scripts": {
    ...
    "test": "vue-cli-service test:unit"
  },
  "devDependencies": {
    ...
    "@vue/cli-plugin-unit-jest": "^4.4.0",
    "@vue/test-utils": "^1.0.3"
  }
}

This adds the dependencies needed to work with Jest in Vue, and it adds a script to execute the tests. Don’t forget to run npm install after modifying the dependencies in package.json.

Also, we create the file jest.config.js to configure Jest to work with *.vue files:

module.exports = {
  preset: "@vue/cli-plugin-unit-jest",
  collectCoverage: true,
  collectCoverageFrom: ["src/**/*.{js,vue}", "!**/node_modules/**"],
  coverageReporters: ["html", "text-summary"]
};

Next, we create a test for our WeekChart component in src/tests/unit/WeekChart.spec.js:

import { shallowMount } from "@vue/test-utils";
import WeekChart from "../../components/WeekChart.vue";

describe("WeekChart", () => {
  it("renders without error", () => {
    const wrapper = shallowMount(WeekChart, {
      propsData: {
      chartData: [1, 2, 3, 4, 5, 6, 7],
      },
    });
  
    const chart = wrapper.findComponent({ name: "WeekChart" });
    expect(chart.exists()).toBe(true);
  });

});

We can run the test with npm run test.

The test will pass, but it will show some error output on the console:

Error: Not implemented: HTMLCanvasElement.prototype.getContext 
  (without installing the canvas npm package)

This is because our chart component relies on a canvas element, which is not supported in the Jest runtime environment. But we want the test to fail in this case! So, we configure the Jest runtime to throw an error when it encounters this error log. For this, we create the file jest/console-error-to-exception.setup.js:

import { format } from "util";

beforeEach(() => {
  const { error } = global.console;

  global.console.error = (...args) => {
    for (let i = 0; i < args.length; i += 1) {
      const arg = args[i];
    
      // add patterns here that should fail a test
      if (typeof arg === "string" &&
           (arg.includes("Vue warn") || arg.includes("Not implemented"))) {
        throw new Error(format(...args));
      }
    }
    error(...args);
  };
});

This will intercept calls to console.error() and re-throw them as an error if they match a certain pattern. The patterns include the “not implemented” error we encountered before and Vue warnings.

We now need to tell Jest to run this code before every test by adding the file to jest.config.js:

module.exports = {
  ...
  setupFilesAfterEnv: [
    "./jest/console-error-to-exception.setup.js"
  ]
};

If we run the test again, it will now fail with the same error message as above. Here’s the source where I got this idea.

To fix the underlying problem of the unavailable canvas element, we add a mock canvas to our development dependencies in package.json:

npm install --save-dev jest-canvas-mock

Also, we add another Jest setup file in /jest/mock-canvas.setup.js with a single import statement:

import 'jest-canvas-mock';

and add this file to jest.config.js to be executed for all tests:

module.exports = {
  ...
  setupFilesAfterEnv: [
    ...
    "./jest/mock-canvas.setup.js"
 ]
};

Now, the tests will have access to a mock Canvas element and the test will be green.

The test will now tell us when we broke something.

Integrating the Vue Build into the Spring Boot Build

We have a Spring Boot application that’s being built with a Gradle process (you can probably also do it with Maven, but I’m a Gradle fanboy) and a Vue component that is built with NPM. We want to include our Vue component in the Spring Boot application so it can serve the Javascript together with the HTML. How do we do that?

The solution I went for is to wrap the Javascript build within Gradle. When the Gradle build starts, it triggers the NPM build, creating ready-for-use Javascript files that we can include in our HTML pages. All we need to do then is to copy those Javascript files to a location where they will be picked up when the Spring Boot application is packaged.

The first step is to make our client folder a module in the Gradle build. For this, we create a file build.gradle in this folder:

plugins {
  id "com.github.node-gradle.node" version "2.2.4"
}

task build(type: NpmTask) {
  inputs.dir("src")
  outputs.dir("dist")
  args = ['run', 'build']
}

We include the Gradle Node Plugin which enables us to call NPM tasks from within our Gradle build.

We create a single task build (the same task name that is used in the Java build) which calls npm run build. We add the src folder as an input and the dist folder as an output. This way, Gradle will only execute the task if an input file has changed or the output files don’t exist.

Since with the server and the client folders we now have a multi-module Gradle build, we need to create a settings.gradle file in the parent directory that lists all the modules:

rootProject.name = 'thymeleaf-vue'

include 'client'
include 'server'

And finally, in the build.gradle file of the server module, we need to add the dependency to the Javascript components:

dependencies {
  project(':client')
  ...
}

task copyJavascriptComponents(type: Copy) {
  from '../client/dist'
  include '**/*.umd.min.js'
  into 'src/main/resources/static/js/vue-components'
}

task cleanJavascriptComponents(type: Delete) {
  delete 'src/main/resources/static/js/vue-components'
}

processResources.dependsOn copyJavascriptComponents
clean.dependsOn cleanJavascriptComponents

We also added the task copyJavascriptComponents which copies all *.umd.min.js files from the client module into the resources folder of the server module and added this task as a dependency to the processResources task so that it will always be executed before the resources are being processed by Gradle. This way, if the Gradle project is built, we can always be sure to get the latest version of the Javascript files. Also, we add a clean task that cleans up after a previous run.

Copying Files Between Gradle Modules

Note that copying files between Gradle modules is not the most elegant solution for adding Javascript dependencies into our Spring Boot application. A cleaner way would be to have the client module export the Javascript files as real Gradle (or NPM) artifacts and declare a dependency on those artifacts within the server module. This way, Gradle would know the dependencies and could probably optimize the build even further.

That said, it's by far the easiest solution and I'm very happy with it in my project, so far.

Using the Vue Component in a Thymeleaf Template

If we build the project now with ./gradlew build, we get a Spring Boot application that carries the file WeekChart.umd.min.js in its belly. That means we can use it in our Thymeleaf template hello-vue.html that we have created at the start of this article:

<html>
  <body>
  <h1 th:text="${title}">This title will be replaced</h1>
  <p> Here comes a Vue component!</p>
  
  <div id="chart">
    <chart th:v-bind:chart-data="${chartData}"></chart>
  </div>
  
  <script src="https://unpkg.com/vue"></script>
  <script th:src="@{/js/vue-components/WeekChart/WeekChart.umd.min.js}"></script>
  <script>
    (function() {
      new Vue({
        components: {
          chart: WeekChart
        }
      }).$mount('#chart')
    })();
  </script>
  
  </body>
</html>

We’ve added a <div> with the id chart that contains an instance of our WeekChart component.

We want to provide the data to the chart from the server, so we add a th: (for “thymeleaf”) in front of the attribute v-bind:chart-data that is expected by vue to pass an array prop into the component. This will let Thymeleaf know that we want this attribute populated with the value of the chartData variable.

Also, we added <script> tags to load Vue.js and our chart component and another one to create the Vue component and bind it to the <chart> tag within the chart div.

Finally, we need to modify our server-side controller so that it populates the chartData variable:

@Controller
class HelloVueController {

  @GetMapping("/hello")
  ModelAndView showHelloPage() {
    Map<String, Object> model = new HashMap<>();
    model.put("title", "Hello Vue!");
    model.put("chartData", Arrays.asList(7,6,5,4,3,2,1));
    return new ModelAndView("hello-vue.html", model);
  }

}

Running ./gradlew bootrun and opening http://localhost:8080/hello in a browser will now proudly show our Vue chart component on the page, populated with data from the server.

Conclusion

In this article, we created a Spring Boot application with the server-side template engine Thymeleaf and a Javascript component library that provides a Javascript component built with NPM and Vue. The result is a hybrid application that allows the server-side template engine to create static HTML pages while including Javascript components that allow more interactivity.

We have established a proper development environment for both, the server-side Java part, and the client-side Javascript part.

There’s certainly more tweaking necessary to get this integration of Vue.js and Thymeleaf customized to a specific project (sharing CSS between client and server, bundling Javascript components together or not, …) but this article has laid the foundation.

I’m using this method in my service at blogtrack.io and might report about its evolution in the future.

A working example including all the bits and pieces that this article glossed over is available on Github.

Follow me on Twitter, LinkedIn, or my Mailing List to be notified of new content.

Get 66% Off My eBook

Get Your Hands Dirty on Clean Architecture

Liked this article? Subscribe to my mailing list to get notified about new content and get 66% off my eBook "Get Your Hands Dirty on Clean Architecture".