Build an App with Spring Boot and Vuejs

Build an App with Spring Boot and Vuejs

In this post, I’ll show you how to build a Spring Boot API using Spring Data JPA and Hibernate. Then, I’ll show you how to create a Vue PWA and customize it to display the data from your API. Then you’ll add in some animated gifs, a sprinkle of authentication, and have a jolly good time doing it!

Vue is a lean and mean JavaScript framework that has become increasingly popular due to the need for speed in the web and mobile revolution. If you learned Angular.js back in the day, then chances are you’ll be drawn to Vue.js.

I’m biased when it comes to Spring Boot — it’s one of my favorite frameworks in the Java ecosystem, and I’ve been a fan since 2004. I’m also a huge fan of Vue, which is a reason why I’m writing this tutorial. Today, I'd like to show you why Vue and Spring Boot are among my list of favorite frameworks.

In this post, I’ll show you how to build a Spring Boot API using Spring Data JPA and Hibernate. Then, I’ll show you how to create a Vue PWA and customize it to display the data from your API. Then you’ll add in some animated gifs, a sprinkle of authentication, and have a jolly good time doing it!

Build a REST API With Spring Boot

To get started with Spring Boot, navigate to start.spring.io and choose version 2.1.1+. In the "Search for dependencies" field, select the following:

  • H2: An in-memory database
  • Lombok: Because no one likes generating (or even worse, writing!) getters and setters
  • JPA: Standard ORM for Java
  • Rest Repositories: Allows you to expose your JPA repositories as REST endpoints
  • Web: Spring MVC with Jackson (for JSON), Hibernate Validator, and embedded Tomcat

If you like the command-line better, install HTTPie and run the following command to download a demo.zip.

http https://start.spring.io/starter.zip dependencies==h2,lombok,data-jpa,data-rest,web \
  packageName==com.okta.developer.demo -d

Create a directory called spring-boot-vue-example. Expand the contents of demo.zip into its server directory.

mkdir spring-boot-vue-example
unzip demo.zip -d spring-boot-vue-example/server

Open the "server" project in your favorite IDE and run DemoApplication or start it from the command line using ./mvnw spring-boot:run.

Create a com.okta.developer.demo.beer package and a Beer.java file in it. This class will be the entity that holds your data.

package com.okta.developer.demo.beer;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Data
@NoArgsConstructor
@Entity
class Beer {

    public Beer(String name) {
        this.name = name;
    }

    @Id
    @GeneratedValue
    private Long id;

    @NonNull
    private String name;
}

Add a BeerRepository class that leverages Spring Data to do CRUD on this entity.

package com.okta.developer.demo.beer;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource
interface BeerRepository extends JpaRepository<Beer, Long> {
}

| Adding the @RepositoryRestResource annotation to BeerRepository exposes all its CRUD operations as REST endpoints. |

Add a BeerCommandLineRunner that uses this repository and creates a default set of data.

package com.okta.developer.demo.beer;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.stream.Stream;

@Component
public class BeerCommandLineRunner implements CommandLineRunner {

    private final BeerRepository repository;

    public BeerCommandLineRunner(BeerRepository repository) {
        this.repository = repository;
    }

    @Override
    public void run(String... strings) throws Exception {
        // Top beers from https://www.beeradvocate.com/lists/us, November 2018
        Stream.of("Kentucky Brunch Brand Stout", "Marshmallow Handjee", "Barrel-Aged Abraxas",
            "Hunahpu's Imperial Stout", "King Julius", "Heady Topper",
            "Budweiser", "Coors Light", "PBR").forEach(name ->
            repository.save(new Beer(name))
        );
        repository.findAll().forEach(System.out::println);
    }
}

Restart your app, and you should see a list of beers printed in your terminal.

Add a BeerController class to create an endpoint that filters out less-than-great beers.

package com.okta.developer.demo.beer;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.stream.Collectors;

@RestController
public class BeerController {
    private BeerRepository repository;

    public BeerController(BeerRepository repository) {
        this.repository = repository;
    }

    @GetMapping("/good-beers")
    public Collection<Beer> goodBeers() {
        return repository.findAll().stream()
                .filter(this::isGreat)
                .collect(Collectors.toList());
    }

    private boolean isGreat(Beer beer) {
        return !beer.getName().equals("Budweiser") &&
                !beer.getName().equals("Coors Light") &&
                !beer.getName().equals("PBR");
    }
}

Re-build your application and navigate to [http://localhost:8080/good-beers](http://localhost:8080/good-beers). You should see the list of good beers in your browser.

You should also see the same result in your terminal window when using HTTPie.

http :8080/good-beers
Create a Project With Vue CLI

Creating an API seems to be the easy part these days, thanks in large part to Spring Boot. In this section, I hope to show you that creating a UI with Vue is pretty simple too. I’ll also show you how to develop the Vue app with TypeScript. If you follow the steps below, you’ll create a new Vue app, fetch beer names and images from APIs, and create components to display their data.

To create a Vue project, make sure you have Node.js, and Vue CLI 3 installed. I used Node 11.3.0 when I created this tutorial.

npm install -g @vue/[email protected]

From a terminal window, cd into the root of the spring-boot-vue-example directory and run the following command. This command will create a new Vue application and prompt you for options.

vue create client

When prompted to pick a present, choose Manually select features.

Check the TypeScript, PWA, and Router features. Choose the defaults (by pressing Enter) for the rest of the questions.

In a terminal window, cd into the client directory and open package.json in your favorite editor. Add a start script that’s the same as the serve script.

"scripts": {
  "start": "vue-cli-service serve",
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint"
},

Now you can start your Vue app using npm start. Your Spring Boot app should be still running on port 8080, which will cause your Vue app to use port 8081. I expect you to run your Vue app on 8081 throughout this tutorial. To ensure it always runs on this port, create a client/vue.config.js file and add the following JavaScript to it.

module.exports = {
  devServer: {
    port: 8081
  }
};

Open [http://localhost:8081](http://localhost:8081/) in your browser, and you should see a page like the one below.

Create a Good Beers UI in Vue

So far, you’ve created a good beers API and a Vue client, but you haven’t created the UI to display the list of beers from your API. To do this, open client/src/views/Home.vue and add a created() method.

import axios from 'axios';
...

private async created() {
  const response = await axios.get('/good-beers');
  this.beers = await response.data;
}

Vue’s component lifecycle will call the created() method.

| John Papa’s Vue.js with TypeScript was a big help in figuring out how to use TypeScript with Vue. Vue’s TypeScript docs were also helpful. |

You’ll need to install axios for this code to compile.

npm i axios

You can see this puts the response data into a local beers variable. To properly define this variable, create a Beer interface and initialize the Home class’s beers variable to be an empty array.

export interface Beer {
  id: number;
  name: string;
  giphyUrl: string;
}

@Component({
  components: {
    HelloWorld,
  },
})
export default class Home extends Vue {
  public beers: Beer[] = [];

  private async created() {
    const response = await axios.get('/good-beers');
    this.beers = await response.data;
  }
}

A keen eye will notice this makes a request to /good-beers on the same port as the Vue application (since it’s a relative URL). For this to work, you’ll need to modify client/vue.config.js to have a proxy that sends this URL to your Spring Boot app.

module.exports = {
  devServer: {
    port: 8081,
    proxy: {
      "/good-beers": {
        target: "http://localhost:8080",
        secure: false
      }
    }
  }
};

Modify the template in client/src/views/Home.vue to display the list of good beers from your API.

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <h1>Beer List</h1>
    <div v-for="beer in beers">
      {{ beer.name }}
    </div>
  </div>
</template>

Restart your Vue app using npm start and refresh your app on [http://localhost:8081](http://localhost:8081/). You should see a list of beers from your Spring Boot API.

Create a BeerList Component

To make this application easier to maintain, move the beer list logic and rendering to its own BeerList component. Create client/src/components/BeerList.vue and populate it with the code from Home.vue. Remove the Vue logo, customize the template’s main class name, and remove the HelloWorld component. It should look as follows when you’re done.

<template>
  <div class="beer-list">
    <h1>Beer List</h1>
    <div v-for="beer in beers">
      {{ beer.name }}
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import axios from 'axios';

export interface Beer {
  id: number;
  name: string;
  giphyUrl: string;
}

@Component
export default class BeerList extends Vue {
  public beers: Beer[] = [];

  private async created() {
    const response = await axios.get('/good-beers');
    this.beers = await response.data;
  }
}
</script>

Then change client/src/views/Home.vue so it only contains the logo and a reference to <BeerList/>.

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <BeerList/>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import BeerList from '@/components/BeerList.vue';

@Component({
  components: {
    BeerList,
  },
})
export default class Home extends Vue {}
</script>
Create a GiphyImage Component

To make things look a little better, add a GIPHY component to fetch images based on the beer’s name. Create client/src/components/GiphyImage.vue and place the following code inside it.

<template>
  <img :src=giphyUrl v-bind:alt=name height="200"/>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import axios from 'axios';

@Component
export default class GiphyImage extends Vue {
  @Prop() private name!: string;
  private giphyUrl: string = '';

  private async created() {
    const giphyApi = '//api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&limit=1&q=';

    const response = await axios.get(giphyApi + this.name);
    const data = await response.data.data;
    if (data.length) {
      this.giphyUrl = data[0].images.original.url;
    } else {
      this.giphyUrl = '//media.giphy.com/media/YaOxRsmrv9IeA/giphy.gif';
    }
  }
}
</script>

<!-- The "scoped" attribute limits CSS to this component only -->
<style scoped>
img {
  margin: 10px 0 0;
}
</style>

Change BeerList.vue to use the <GiphyImage/> component in its template:

<div v-for="beer in beers">
  {{ beer.name }}<br/>
  <GiphyImage :name="beer.name"/>
</div>

And add it to the components list in the <script> block:

import GiphyImage from '@/components/GiphyImage.vue';

@Component({
  components: {GiphyImage},
})
export default class BeerList extends Vue { ... }

In this same file, add a <style> section at the bottom and use CSS Grid layout to organize the beers in rows.

<style scoped>
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 10px;
  grid-auto-rows: minmax(100px, auto);
}
</style>

You’ll need to wrap a div around the beer list template for this to have any effect.

<div class="grid">
  <div v-for="beer in beers">
    {{ beer.name }}<br/>
    <GiphyImage :name="beer.name"/>
  </div>
</div>

After making these changes, your UI should look something like the following list of beer names and matching images.

You just created a Vue app that talks to a Spring Boot API. Congratulations!

Add PWA Support

Vue CLI has support for progressive web applications (PWAs) out-of-the-box. When you created your Vue app, you selected PWA as a feature.

PWA features are only enabled in production, because having assets cached in development can be a real pain. Run npm run build in the client directory to create a build ready for production. Then use serve to create a web server and show your app.

npm i -g serve
serve -s dist -p 8081

You should be able to open your browser and see your app at [http://localhost:8081](http://localhost:8081/). When I first tried this, I found that loading the page didn’t render any beer names and all the images were the same. This is because the client attempts to make a request to /good-beers and there’s no proxy configured in production mode.

To fix this issue, you’ll need to change the URL in the client and configure Spring Boot to allow cross-domain access from [http://localhost:8081](http://localhost:8081/).

Modify client/src/components/BeerList.vue to use the full URL to your Spring Boot API.

private async created() {
  const response = await axios.get('http://localhost:8080/good-beers');
  this.beers = await response.data;
}

| If you don’t see any changes in your UI after making these changes, it’s because your browser has cached your app. Use an incognito window, or clear your cache (in Chrome: Developer Tools > Application > Clear storage> Clear site data) to fix this issue. |

Configure CORS for Spring Boot

In the server project, open src/main/java/…/demo/beer/BeerController.java and add a @CrossOrigin annotation to enable cross-origin resource sharing (CORS) from the client ([http://localhost:8081](http://localhost:8081/)).

import org.springframework.web.bind.annotation.CrossOrigin;
...
    @GetMapping("/good-beers")
    @CrossOrigin(origins = "http://localhost:8081")
    public Collection<Beer> goodBeers() {

After making these changes, rebuild your Vue app for production, refresh your browser, and everything should render as expected.

Use Lighthouse to See Your PWA Score

I ran a Lighthouse audit in Chrome and found that this app scores a 81/100 at this point. The most prominent complaint from this report was that I wasn’t using HTTPS. To see how the app would score when it used HTTPS, I deployed it to Pivotal Cloud Foundry and Heroku. I was pumped to discover it scored high on both platforms.

The reason it scores a 96 is because The viewport size is 939px, whereas the window size is 412px. I’m not sure what’s causing this issue, maybe it’s the CSS Grid layout?

To see the scripts I used to deploy everything, see heroku.sh and cloudfoundry.sh in this post’s companion GitHub repository.

| You will need to initialize Git before running the deployment scripts. Run rm -rf client/.git, followed by git commit -a "Add project". |

Okta Spring Boot Starter

To secure your API, you can use Okta’s Spring Boot Starter. To integrate this starter, add the following dependencies to server/pom.xml:

<dependency>
    <groupId>com.okta.spring</groupId>
    <artifactId>okta-spring-boot-starter</artifactId>
    <version>0.6.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.1.RELEASE</version>
</dependency>

Now you need to configure the server to use Okta for authentication. You’ll need to create an OIDC app in Okta for that.

Create an OIDC App in Okta

Log in to your Okta Developer account (or sign up if you don’t have an account) and navigate to Applications > Add Application. Click Single-Page App, click Next, and give the app a name you’ll remember. Change all instances of localhost:8080 to localhost:8081 and click Done.

Copy the client ID into your server/src/main/resources/application.properties file. While you’re in there, add a okta.oauth2.issuer property that matches your Okta domain. For example:

okta.oauth2.issuer=https://okta.okta.com/oauth2/default
okta.oauth2.client-id={yourClientId}

| Replace {yourOktaDomain} with your org URL, which you can find on the Dashboard of the Developer Console. Make sure you don’t include -admin in the value! |

Update server/src/main/java/…/demo/DemoApplication.java to enable it as a resource server.

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

@EnableResourceServer
@SpringBootApplication

After making these changes, you should be able to restart the server and see access denied when you try to navigate to [http://localhost:8080](http://localhost:8080/).

Okta’s Vue Support

Okta’s Vue SDK allows you to integrate OIDC into a Vue application. You can learn more about Okta’s Vue SDK can be found on npmjs.com. To install, run the following commands in the client directory:

npm i @okta/[email protected]
npm i -D @types/okta__okta-vue

| The types for Okta’s Vue SDK may be included in a future release. I created a pull request to add them. |

Open client/src/router.ts and add your Okta configuration. The router.ts below also includes a path for the BeerList, a callback that’s required for authentication, and a navigation guard to require authentication for the /beer-listpath. Replace yours with this one, then update yourClientDomain and yourClientId to match your settings. Make sure to remove the {} since those are just placeholders.

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import OktaVuePlugin from '@okta/okta-vue';
import BeerList from '@/components/BeerList.vue';

Vue.use(Router);
Vue.use(OktaVuePlugin, {
  issuer: 'https://okta.okta.com/oauth2/default',
  client_id: '{yourClientId}',
  redirect_uri: window.location.origin + '/implicit/callback',
  scope: 'openid profile email',
});

const router = new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
    },
    {
      path: '/beer-list',
      name: 'beer-list',
      component: BeerList,
      meta: {
        requiresAuth: true,
      },
    },
    { path: '/implicit/callback', component: OktaVuePlugin.handleCallback() },
  ],
});

router.beforeEach(Vue.prototype.$auth.authRedirectGuard());

export default router;

Since you have a route for BeerList remove it from client/src/views/Home.vue.

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class Home extends Vue {}
</script>

Add a link to the BeerList in client/src/App.vue. You’ll also need to add code that detects if the user is logged in or not. Replace the <template> section and add the <script> below to your App.vue.

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
      <template v-if="authenticated"> |
        <router-link to="/beer-list">Good Beers</router-link>
      </template>
    </div>
    <button v-if="authenticated" v-on:click="logout">Logout</button>
    <button v-else v-on:click="$auth.loginRedirect()">Login</button>
    <router-view/>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';

@Component
export default class App extends Vue {
  public authenticated: boolean = false;

  private created() {
    this.isAuthenticated();
  }

  @Watch('$route')
  private async isAuthenticated() {
    this.authenticated = await this.$auth.isAuthenticated();
  }

  private async logout() {
    await this.$auth.logout();
    await this.isAuthenticated();

    // Navigate back to home
    this.$router.push({path: '/'});
  }
}
</script>

Restart your Vue app and you should see a button to log in.

Click on it and you’ll be redirected to Okta. Enter the credentials you used to sign up for Okta and you’ll be redirected back to the app. You should see a Logout button and a link to see some good beers.

If you click on the Good Beers link, you’ll see the component’s header, but no data. If you look at your JavaScript console, you’ll see there’s a CORS error.

This error happens because Spring’s @CrossOrigin doesn’t play well with Spring Security. To solve this problem, add a simpleCorsFilter bean to the body of DemoApplication.java.

package com.okta.developer.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Collections;

@EnableResourceServer
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public FilterRegistrationBean<CorsFilter> simpleCorsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOrigins(Collections.singletonList("http://localhost:8081"));
        config.setAllowedMethods(Collections.singletonList("*"));
        config.setAllowedHeaders(Collections.singletonList("*"));
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

Restart your server after making this change. To make it all work on the client, modify the created() method in client/src/components/BeerList.vue to set an authorization header.

private async created() {
  const response = await axios.get('http://localhost:8080/good-beers',
    {
      headers: {
        Authorization: `Bearer ${await this.$auth.getAccessToken()}`,
      },
    },
  );
  this.beers = await response.data;
}

Now you should be able to see the good beer list as an authenticated user.

If it works, excellent!

Learn More About Spring Boot and Vue

This tutorial showed you how to build an app that uses modern frameworks like Spring Boot and Vue. You learned how to add authentication with OIDC and protect routes using Okta’s Vue SDK. If you’d like to watch a video of this tutorial, I published it as a screencast to YouTube.

If you want to learn more about the Vue phenomenon, I have a couple of recommended articles. First of all, I think it’s awesome it’s not sponsored by a company (like Angular + Google and React + Facebook), and that’s it’s mostly community-driven. The Solo JavaScript Developer Challenging Google and Facebook is an article in Wired that explains why this is so amazing.

Regarding JavaScript framework performance, The Baseline Costs of JavaScript Frameworks is an interesting blog post from Anku Sethi. I like his motivation for writing it:

"Last week, I was curious about how much of a performance impact just including React on a page can have. So I ran some numbers on a cheap Android phone and wrote about it."

You can find the source code associated with this article on GitHub. The primary example (without authentication) is in the master branch, while the Okta integration is in the okta branch. To check out the Okta branch on your local machine, run the following command.

git clone -b okta https://github.com/oktadeveloper/spring-boot-vue-example.git

If you find any issues, please add a comment, and I’ll do my best to help.

How to build Vue.js JWT Authentication with Vuex and Vue Router

How to build Vue.js JWT Authentication with Vuex and Vue Router

In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication

In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication. I will show you:

  • JWT Authentication Flow for User Signup & User Login
  • Project Structure for Vue.js Authentication with Vuex & Vue Router
  • How to define Vuex Authentication module
  • Creating Vue Authentication Components with Vuex Store & VeeValidate
  • Vue Components for accessing protected Resources
  • How to add a dynamic Navigation Bar to Vue App

Let’s explore together.

Contents

Overview of Vue JWT Authentication example

We will build a Vue application in that:

  • There are Login/Logout, Signup pages.
  • Form data will be validated by front-end before being sent to back-end.
  • Depending on User’s roles (admin, moderator, user), Navigation Bar changes its items automatically.

Screenshots

– Signup Page:

– Login Page & Profile Page (for successful Login):

– Navigation Bar for Admin account:

Demo

This is full Vue JWT Authentication App demo (with form validation, check signup username/email duplicates, test authorization with 3 roles: Admin, Moderator, User). In the video, we use Spring Boot for back-end REST APIs.

Flow for User Registration and User Login

For JWT Authentication, we’re gonna call 2 endpoints:

  • POST api/auth/signup for User Registration
  • POST api/auth/signin for User Login

You can take a look at following flow to have an overview of Requests and Responses Vue Client will make or receive.

Vue Client must add a JWT to HTTP Authorization Header before sending request to protected resources.

Vue App Component Diagram with Vuex & Vue Router

Now look at the diagram below.

Let’s think about it.

– The App component is a container with Router. It gets app state from Vuex store/auth. Then the navbar now can display based on the state. App component also passes state to its child components.

Login & Register components have form for submission data (with support of vee-validate). We call Vuex store dispatch() function to make login/register actions.

– Our Vuex actions call auth.service methods which use axios to make HTTP requests. We also store or get JWT from Browser Local Storage inside these methods.

Home component is public for all visitor.

Profile component get user data from its parent component and display user information.

BoardUser, BoardModerator, BoardAdmin components will be displayed by Vuex state user.roles. In these components, we use user.service to get protected resources from API.

user.service uses auth-header() helper function to add JWT to HTTP Authorization header. auth-header() returns an object containing the JWT of the currently logged in user from Local Storage.

Technology

We will use these modules:

  • vue: 2.6.10
  • vue-router: 3.0.3
  • vuex: 3.0.1
  • axios: 0.19.0
  • vee-validate: 2.2.15
  • bootstrap: 4.3.1
  • vue-fontawesome: 0.1.7
Project Structure

This is folders & files structure for our Vue application:

With the explaination in diagram above, you can understand the project structure easily.

Setup Vue App modules

Run following command to install neccessary modules:

npm install vue-router
npm install vuex
npm install [email protected]
npm install axios
npm install bootstrap jquery popper.js
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome

After the installation is done, you can check dependencies in package.json file.

"dependencies": {
  "@fortawesome/fontawesome-svg-core": "^1.2.25",
  "@fortawesome/free-solid-svg-icons": "^5.11.2",
  "@fortawesome/vue-fontawesome": "^0.1.7",
  "axios": "^0.19.0",
  "bootstrap": "^4.3.1",
  "core-js": "^2.6.5",
  "jquery": "^3.4.1",
  "popper.js": "^1.15.0",
  "vee-validate": "^2.2.15",
  "vue": "^2.6.10",
  "vue-router": "^3.0.3",
  "vuex": "^3.0.1"
},

Open src/main.js, add code below:

import Vue from 'vue';
import App from './App.vue';
import { router } from './router';
import store from './store';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import VeeValidate from 'vee-validate';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import {
  faHome,
  faUser,
  faUserPlus,
  faSignInAlt,
  faSignOutAlt
} from '@fortawesome/free-solid-svg-icons';

library.add(faHome, faUser, faUserPlus, faSignInAlt, faSignOutAlt);

Vue.config.productionTip = false;

Vue.use(VeeValidate);
Vue.component('font-awesome-icon', FontAwesomeIcon);

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');

You can see that we import and apply in Vue object:
store for Vuex (implemented later in src/store)
router for Vue Router (implemented later in src/router.js)
bootstrap with CSS
vee-validate
vue-fontawesome for icons (used later in nav)

Create Services

We create two services in src/services folder:


services

auth-header.js

auth.service.js (Authentication service)

user.service.js (Data service)


Authentication service

The service provides three important methods with the help of axios for HTTP requests & reponses:

  • login(): POST {username, password} & save JWT to Local Storage
  • logout(): remove JWT from Local Storage
  • register(): POST {username, email, password}
import axios from 'axios';

const API_URL = 'http://localhost:8080/api/auth/';

class AuthService {
  login(user) {
    return axios
      .post(API_URL + 'signin', {
        username: user.username,
        password: user.password
      })
      .then(this.handleResponse)
      .then(response => {
        if (response.data.accessToken) {
          localStorage.setItem('user', JSON.stringify(response.data));
        }

        return response.data;
      });
  }

  logout() {
    localStorage.removeItem('user');
  }

  register(user) {
    return axios.post(API_URL + 'signup', {
      username: user.username,
      email: user.email,
      password: user.password
    });
  }

  handleResponse(response) {
    if (response.status === 401) {
      this.logout();
      location.reload(true);

      const error = response.data && response.data.message;
      return Promise.reject(error);
    }

    return Promise.resolve(response);
  }
}

export default new AuthService();

If login request returns 401 status (Unauthorized), that means, JWT was expired or no longer valid, we will logout the user (remove JWT from Local Storage).

Data service

We also have methods for retrieving data from server. In the case we access protected resources, the HTTP request needs Authorization header.

Let’s create a helper function called authHeader() inside auth-header.js:

export default function authHeader() {
  let user = JSON.parse(localStorage.getItem('user'));

  if (user && user.accessToken) {
    return { Authorization: 'Bearer ' + user.accessToken };
  } else {
    return {};
  }
}

It checks Local Storage for user item.
If there is a logged in user with accessToken (JWT), return HTTP Authorization header. Otherwise, return an empty object.

Now we define a service for accessing data in user.service.js:

import axios from 'axios';
import authHeader from './auth-header';

const API_URL = 'http://localhost:8080/api/test/';

class UserService {
  getPublicContent() {
    return axios.get(API_URL + 'all');
  }

  getUserBoard() {
    return axios.get(API_URL + 'user', { headers: authHeader() });
  }

  getModeratorBoard() {
    return axios.get(API_URL + 'mod', { headers: authHeader() });
  }

  getAdminBoard() {
    return axios.get(API_URL + 'admin', { headers: authHeader() });
  }
}

export default new UserService();

You can see that we add a HTTP header with the help of authHeader() function when requesting authorized resource.

Define Vuex Authentication module

We put Vuex module for authentication in src/store folder.


store

auth.module.js (authentication module)

index.js (Vuex Store that contains also modules)


Now open index.js file, import auth.module to main Vuex Store here.

import Vue from 'vue';
import Vuex from 'vuex';

import { auth } from './auth.module';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    auth
  }
});

Then we start to define Vuex Authentication module that contains:

  • state: { status, user }
  • actions: { login, logout, register }
  • mutations: { loginSuccess, loginFailure, logout, registerSuccess, registerFailure }

We use AuthService which is defined above to make authentication requests.

auth.module.js

import AuthService from '../services/auth.service';

const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
  ? { status: { loggedIn: true }, user }
  : { status: {}, user: null };

export const auth = {
  namespaced: true,
  state: initialState,
  actions: {
    login({ commit }, user) {
      return AuthService.login(user).then(
        user => {
          commit('loginSuccess', user);
          return Promise.resolve(user);
        },
        error => {
          commit('loginFailure');
          return Promise.reject(error.response.data);
        }
      );
    },
    logout({ commit }) {
      AuthService.logout();
      commit('logout');
    },
    register({ commit }, user) {
      return AuthService.register(user).then(
        response => {
          commit('registerSuccess');
          return Promise.resolve(response.data);
        },
        error => {
          commit('registerFailure');
          return Promise.reject(error.response.data);
        }
      );
    }
  },
  mutations: {
    loginSuccess(state, user) {
      state.status = { loggedIn: true };
      state.user = user;
    },
    loginFailure(state) {
      state.status = {};
      state.user = null;
    },
    logout(state) {
      state.status = {};
      state.user = null;
    },
    registerSuccess(state) {
      state.status = {};
    },
    registerFailure(state) {
      state.status = {};
    }
  }
};

You can find more details about Vuex at Vuex Guide.

Create Vue Authentication Components

Define User model

To make code clear and easy to read, we define the User model first.
Under src/models folder, create user.js like this.

export default class User {
  constructor(username, email, password) {
    this.username = username;
    this.email = email;
    this.password = password;
  }
}

Let’s continue with Authentication Components.
Instead of using axios or AuthService directly, these Components should work with Vuex Store:
– getting status with this.$store.state.auth
– making request by dispatching an action: this.$store.dispatch()


views

Login.vue

Register.vue

Profile.vue


Vue Login Page

In src/views folder, create Login.vue file with following code:

<template>
  <div class="col-md-12">
    <div class="card card-container">
      <img
        id="profile-img"
        src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
        class="profile-img-card"
      />
      <form name="form" @submit.prevent="handleLogin">
        <div class="form-group">
          <label for="username">Username</label>
          <input
            type="text"
            class="form-control"
            name="username"
            v-model="user.username"
            v-validate="'required'"
          />
          <div
            class="alert alert-danger"
            role="alert"
            v-if="errors.has('username')"
          >Username is required!</div>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input
            type="password"
            class="form-control"
            name="password"
            v-model="user.password"
            v-validate="'required'"
          />
          <div
            class="alert alert-danger"
            role="alert"
            v-if="errors.has('password')"
          >Password is required!</div>
        </div>
        <div class="form-group">
          <button class="btn btn-primary btn-block" :disabled="loading">
            <span class="spinner-border spinner-border-sm" v-show="loading"></span>
            <span>Login</span>
          </button>
        </div>
        <div class="form-group">
          <div class="alert alert-danger" role="alert" v-if="message">{{message}}</div>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import User from '../models/user';

export default {
  name: 'login',
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    }
  },
  data() {
    return {
      user: new User('', ''),
      loading: false,
      message: ''
    };
  },
  mounted() {
    if (this.loggedIn) {
      this.$router.push('/profile');
    }
  },
  methods: {
    handleLogin() {
      this.loading = true;
      this.$validator.validateAll();

      if (this.errors.any()) {
        this.loading = false;
        return;
      }

      if (this.user.username && this.user.password) {
        this.$store.dispatch('auth/login', this.user).then(
          () => {
            this.$router.push('/profile');
          },
          error => {
            this.loading = false;
            this.message = error.message;
          }
        );
      }
    }
  }
};
</script>

<style scoped>
label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}
</style>

This page has a Form with username & password. We use [VeeValidate 2.x](http://<a href=) to validate input before submitting the form. If there is an invalid field, we show the error message.

We check user logged in status using Vuex Store: this.$store.state.auth.status.loggedIn. If the status is true, we use Vue Router to direct user to Profile Page:

created() {
  if (this.loggedIn) {
    this.$router.push('/profile');
  }
},

In the handleLogin() function, we dispatch 'auth/login' Action to Vuex Store. If the login is successful, go to Profile Page, otherwise, show error message.

Vue Register Page

This page is similar to Login Page.

For form validation, we have some more details:

  • username: required|min:3|max:20
  • email: required|email|max:50
  • password: required|min:6|max:40

For form submission, we dispatch 'auth/register' Vuex Action.

src/views/Register.vue

<template>
  <div class="col-md-12">
    <div class="card card-container">
      <img
        id="profile-img"
        src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
        class="profile-img-card"
      />
      <form name="form" @submit.prevent="handleRegister">
        <div v-if="!successful">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              class="form-control"
              name="username"
              v-model="user.username"
              v-validate="'required|min:3|max:20'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('username')"
            >{{errors.first('username')}}</div>
          </div>
          <div class="form-group">
            <label for="email">Email</label>
            <input
              type="email"
              class="form-control"
              name="email"
              v-model="user.email"
              v-validate="'required|email|max:50'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('email')"
            >{{errors.first('email')}}</div>
          </div>
          <div class="form-group">
            <label for="password">Password</label>
            <input
              type="password"
              class="form-control"
              name="password"
              v-model="user.password"
              v-validate="'required|min:6|max:40'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('password')"
            >{{errors.first('password')}}</div>
          </div>
          <div class="form-group">
            <button class="btn btn-primary btn-block">Sign Up</button>
          </div>
        </div>
      </form>

      <div
        class="alert"
        :class="successful ? 'alert-success' : 'alert-danger'"
        v-if="message"
      >{{message}}</div>
    </div>
  </div>
</template>

<script>
import User from '../models/user';

export default {
  name: 'register',
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    }
  },
  data() {
    return {
      user: new User('', '', ''),
      submitted: false,
      successful: false,
      message: ''
    };
  },
  mounted() {
    if (this.loggedIn) {
      this.$router.push('/profile');
    }
  },
  methods: {
    handleRegister() {
      this.message = '';
      this.submitted = true;
      this.$validator.validate().then(valid => {
        if (valid) {
          this.$store.dispatch('auth/register', this.user).then(
            data => {
              this.message = data.message;
              this.successful = true;
            },
            error => {
              this.message = error.message;
              this.successful = false;
            }
          );
        }
      });
    }
  }
};
</script>

<style scoped>
label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}
</style>

Profile Page

This page gets current User from Vuex Store and show information. If the User is not logged in, it directs to Login Page.

src/views/Profile.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>
        <strong>{{currentUser.username}}</strong> Profile
      </h3>
    </header>
    <p>
      <strong>Token:</strong>
      {{currentUser.accessToken.substring(0, 20)}} ... {{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}}
    </p>
    <p>
      <strong>Id:</strong>
      {{currentUser.id}}
    </p>
    <p>
      <strong>Email:</strong>
      {{currentUser.email}}
    </p>
    <strong>Authorities:</strong>
    <ul>
      <li v-for="(role,index) in currentUser.roles" :key="index">{{role}}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'profile',
  computed: {
    currentUser() {
      return this.$store.state.auth.user;
    }
  },
  mounted() {
    if (!this.currentUser) {
      this.$router.push('/login');
    }
  }
};
</script>

Create Vue Components for accessing Resources

These components will use UserService to request data.


views

Home.vue

BoardAdmin.vue

BoardModerator.vue

BoardUser.vue


Home Page

This is a public page.

src/views/Home.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>{{content}}</h3>
    </header>
  </div>
</template>

<script>
import UserService from '../services/user.service';

export default {
  name: 'home',
  data() {
    return {
      content: ''
    };
  },
  mounted() {
    UserService.getPublicContent().then(
      response => {
        this.content = response.data;
      },
      error => {
        this.content = error.response.data.message;
      }
    );
  }
};
</script>

Role-based Pages

We have 3 pages for accessing protected data:

  • BoardUser page calls UserService.getUserBoard()
  • BoardModerator page calls UserService.getModeratorBoard()
  • BoardAdmin page calls UserService.getAdminBoard()

This is an example, other Page are similar to this Page.

src/views/BoardUser.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>{{content}}</h3>
    </header>
  </div>
</template>

<script>
import UserService from '../services/user.service';

export default {
  name: 'user',
  data() {
    return {
      content: ''
    };
  },
  mounted() {
    UserService.getUserBoard().then(
      response => {
        this.content = response.data;
      },
      error => {
        this.content = error.response.data.message;
      }
    );
  }
};
</script>

Define Routes for Vue Router

Now we define all routes for our Vue Application.

src/router.js

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import Login from './views/Login.vue';
import Register from './views/Register.vue';

Vue.use(Router);

export const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/home',
      component: Home
    },
    {
      path: '/login',
      component: Login
    },
    {
      path: '/register',
      component: Register
    },
    {
      path: '/profile',
      name: 'profile',
      // lazy-loaded
      component: () => import('./views/Profile.vue')
    },
    {
      path: '/admin',
      name: 'admin',
      // lazy-loaded
      component: () => import('./views/BoardAdmin.vue')
    },
    {
      path: '/mod',
      name: 'moderator',
      // lazy-loaded
      component: () => import('./views/BoardModerator.vue')
    },
    {
      path: '/user',
      name: 'user',
      // lazy-loaded
      component: () => import('./views/BoardUser.vue')
    }
  ]
});

Add Navigation Bar to Vue App

This is the root container for our application that contains navigation bar. We will add router-view here.

src/App.vue

<template>
  <div id="app">
    <nav class="navbar navbar-expand navbar-dark bg-dark">
      <a href="#" class="navbar-brand">bezKoder</a>
      <div class="navbar-nav mr-auto">
        <li class="nav-item">
          <a href="/home" class="nav-link">
            <font-awesome-icon icon="home" /> Home
          </a>
        </li>
        <li class="nav-item" v-if="showAdminBoard">
          <a href="/admin" class="nav-link">Admin Board</a>
        </li>
        <li class="nav-item" v-if="showModeratorBoard">
          <a href="/mod" class="nav-link">Moderator Board</a>
        </li>
        <li class="nav-item">
          <a href="/user" class="nav-link" v-if="currentUser">User</a>
        </li>
      </div>

      <div class="navbar-nav ml-auto" v-if="!currentUser">
        <li class="nav-item">
          <a href="/register" class="nav-link">
            <font-awesome-icon icon="user-plus" /> Sign Up
          </a>
        </li>
        <li class="nav-item">
          <a href="/login" class="nav-link">
            <font-awesome-icon icon="sign-in-alt" /> Login
          </a>
        </li>
      </div>

      <div class="navbar-nav ml-auto" v-if="currentUser">
        <li class="nav-item">
          <a href="/profile" class="nav-link">
            <font-awesome-icon icon="user" />
            {{currentUser.username}}
          </a>
        </li>
        <li class="nav-item">
          <a href class="nav-link" @click="logOut">
            <font-awesome-icon icon="sign-out-alt" /> LogOut
          </a>
        </li>
      </div>
    </nav>

    <div class="container">
      <router-view />
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    currentUser() {
      return this.$store.state.auth.user;
    },
    showAdminBoard() {
      if (this.currentUser) {
        return this.currentUser.roles.includes('ROLE_ADMIN');
      }

      return false;
    },
    showModeratorBoard() {
      if (this.currentUser) {
        return this.currentUser.roles.includes('ROLE_MODERATOR');
      }

      return false;
    }
  },
  methods: {
    logOut() {
      this.$store.dispatch('auth/logout');
      this.$router.push('/login');
    }
  }
};
</script>

Our navbar looks more professional when using font-awesome-icon.
We also make the navbar dynamically change by current User’s roles which are retrieved from Vuex Store state.

Handle Unauthorized Access

If you want to check Authorized status everytime a navigating action is trigger, just add router.beforeEach() at the end of src/router.js like this:

router.beforeEach((to, from, next) => {
  const publicPages = ['/login', '/home'];
  const authRequired = !publicPages.includes(to.path);
  const loggedIn = localStorage.getItem('user');

  // try to access a restricted page + not logged in
  if (authRequired && !loggedIn) {
    return next('/login');
  }

  next();
});

Conclusion

Congratulation!

Today we’ve done so many interesting things. I hope you understand the overall layers of our Vue application, and apply it in your project at ease. Now you can build a front-end app that supports JWT Authentication with Vue.js, Vuex and Vue Router.

Happy learning, see you again!

Performance Boost Using Highcharts.js and Vue.js

Performance Boost Using Highcharts.js and Vue.js

How to Maintain Performance with Big Datasets Using Highcharts. js and Vue. Sometimes you just need to show big datasets in your project. However, the library that you've used so far, as soon as you start to add data, becomes clunky and slow.

Sometimes you just need to show big datasets in your project. However, the library that you’ve used so far, as soon as you start to add data, becomes clunky and slow. You start to optimize your code, make cuts, or simplifications in datasets… In this article I’m going to show you that it does not have to be that way

Used by “80% of the largest companies in the world”, Highcharts is a powerful tool that allows you to plot massive series of data in a dynamic manner. As it’s free for non-commercial use, you can easily check for yourself if it’s going to suit your tastes. This project will also use highcharts-vue, which is a wrapper for Highcharts.

Installation:

The tools that we are going to need for the job are:

  • Highcharts
  • Highcharts vue
  • Axios

You can easily install them all at once using npm command:

npm i highcharts highcharts-vue axios -S

Now that we have all the pieces, let’s add them in the main.js file:

import Highcharts from "highcharts";
import HighchartsVue from "highcharts-vue";
import boost from "highcharts/modules/boost";

Vue.use(HighchartsVue);
boost(Highcharts);

You may be wondering, what this boost module is. I think that, to best explain it, the official documentation can be cited:

Boost is a stripped-down renderer-in-a-module for Highcharts and Highstock. It bypasses some of the standard Highcharts features (such as animation), and focuses on pushing as many points as possible as quickly as possible.

So, to sum it up, this module will make our charts run faster, but at the cost of some minor drawbacks. For example, you won’t be able to use dotted lines for your series. But, don’t you worry, the key parts are still fully functional.

Disclaimer: As the boost module is still in development, it can cause several bugs. For example, when using timestamps as x value, boost.useGPUTranslations will cause points to render in wrong places while zoomed, so you will need to do additional research, or just ask on the Highcharts forums.

Mocking the data:

As we need to have some data to show, and you probably don’t have any endpoints hanging around, we will need to generate some dummy data. We will need to create an object that has two (or more) attributes filled with chart data (for the sake of showing performance - about 100k points each, but you can totally try with 200-300k, or more). Highcharts allows us to add such as series in two ways:

  • array of {x,y} objects
  • array of values (where you specify the start of each series, as well as intervals)

This article covers how to make use of the first way, but you can always read the official Highcharts documentation to see other possibilities. To get the data, if you don’t want to generate it yourself, you can use my test.json (it’s in a public folder on the repo).

Let’s make use of it:

Now that we have some data to show, we can filtrate it. Even though this is a simplified version of the real application, we can create our architecture in a way that somewhat reflects the real case. Therefore, we can create 3 files:

  • ChartFiltration.vue
  • Chart.vue
  • store.js

ChartFiltration.vue will be, a parent of a Chart component. Its job will be issuing fetches and to know when the data is ready. Chart.vue will simply be used to show the series that are being passed by props. All logic related to processing the chart data will be stored in a store module.

First App.vue

<template>
  <div id="app">
    <ChartFiltration />
  </div>
</template>

<script>
import ChartFiltration from "./components/ChartFiltration";

export default {
  components: { ChartFiltration }
};
</script>

Nothing too fancy here, just some imports.

ChartFiltration.vue

<template>
  <div v-if="series">
    <Chart :series="series" />
  </div>
  <div v-else>
    Data is loading
  </div>
</template>

<script>
import { mapState } from "vuex";
import Chart from "./Chart";

export default {
  name: "ChartFiltration",
  components: { Chart },
  mounted() {
    this.$store.dispatch("fetchData");
  },
  computed: mapState({
    series: state => state.data
  })
};
</script>

As you can see, when the component is being mounted, the fetch in the store will be dispatched. The data will be mapped to a computed series. As I use conditional rendering, Chart.vue component will be rendered only when a series will have its length (because they will be returned as an array).

store.js

import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";

Vue.use(Vuex);

const RESET_ERROR = null;
const generateSeries = data => {
  return Object.keys(data).map(attribute => {
    return {
      type: "line",
      boostThreshold: 1,
      turboThreshold: 0,
      animation: false,
      name: attribute,
      data: data[attribute],
      marker: { enabled: true, radius: 3 },
      states: { hover: { lineWidthPlus: 0 } }
    };
  });
};
export default new Vuex.Store({
  state: {
    data: null,
    error: null
  },
  mutations: {
    SET_DATA(state, data) {
      state.data = generateSeries(data);
    },
    SET_ERROR(state, error) {
      state.error = error;
    }
  },
  actions: {
    async fetchData({ commit }) {
      commit("SET_ERROR", RESET_ERROR);

      axios
        .get("/test.json")
        .then(res => {
          commit("SET_DATA", res.data);
        })
        .catch(err => {
          commit("SET_ERROR", err);
        });
    }
  }
});

In this article I simply want to show how to work with data in Highcharts. There is no need to use modules for the store, like we would in a real application and, therefore, this is why I will use a single store file. Let’s focus on fetchData action. I simulate the http call with axios. After the data has been “fetched”, we will commit the SET_DATA mutation. Highcharts has its specific way of building chart objects. Because there can’t be too much logic in mutations, we add an additional helper function to process our response properly.

const generateSeries = data => {
  return Object.keys(data).map(attribute => {
    return {
      type: "line",
      boostThreshold: 1,
      turboThreshold: 0,
      animation: false,
      name: attribute,
      data: data[attribute],
      marker: { enabled: true, radius: 3 },
      states: { hover: { lineWidthPlus: 0 } }
    };
  });
};

The helper function simply iterates on object to get its attributes. After each iteration, we return the Highcharts series object, and this is what we will focus on:

  • type: here you can specify a type of series
  • boostThreshold: point threshold for when a series should enter boost mode. 1 means that we always want to use boost, 0 means never
  • turboThreshold: for a bigger {x,y} series, it’s better to disable it (by setting 0) as it will not render. If the set threshold is passed, only one dimensional arrays of numbers, or two dimensional arrays with x and y values, are allowed
  • animation: as animations slow down our chart, we don’t want them
  • name: name of the series shown in the legend
  • data: here, we need to assign our array of datapoints
  • marker: size of the markers on the chart
  • states: here, we will disable the line thickening on hover, as it will add lag for big datasets

What can we set additionally ?

  • color: remember that, when using the boost module, you can only use simple colors (rgb/hex). You can also set them with css, but it’s trickier.
  • Honestly, whatever you want, as this is a simple object that you will have access to by using this keyword in the chart object (for example, in the tooltip formatter). You can add units for series, or another type of name to show, but remember that you will need to handle logic for those by yourself.

Here is a list of attributes that you can officially assign for a series.

Now that we have our data processed, we can focus on showing it.

Chart.vue

<template>
  <highcharts :options="chartOptions" />
</template>
<script>
export default {
  name: "Chart",
  props: {
    series: { type: Array, required: true }
  },
  computed: {
    chartOptions() {
      return {
        chart: {
          type: "line",
          height: "700px",
          redraw: true,
          animation: false,
          zoomType: "xy",
          panning: true,
          panKey: "shift"
        },
        boost: { enabled: true },
        tooltip: {
          formatter() {
            const time = new Date(this.x).toJSON();
            const name = this.series.name;
            return `<b>${name}</b><br />
              value: ${this.y} <br />
              time: ${time}`;
          }
        },
        series: this.series,
        xAxis: {
          type: "datetime"
        }
      };
    }
  }
};
</script>

This components sole function is to create a chartOptions object that is passed to the highcharts-vue wrapper. We pass the series as props. On each change, a new chartOptions object will be computed and will force the whole component to rerender. Let’s look at the function that creates it:

chartOptions() {
      return {
        chart: {
          type: "line",
          height: "700px",
          animation: false,
          zoomType: "xy",
          panning: true,
          panKey: "shift"
        },
        boost: { enabled: true },
        tooltip: {
          formatter() {
            const time = new Date(this.x).toJSON();
            const name = this.series.name;
            return `<b>${name}</b><br />
              value: ${this.y} <br />
              time: ${time}`;
          }
        },
        series: this.series,
        xAxis: {
          type: "datetime"
        }
      };
    }

  • chart: has basic information about chart.
    • type: specifies which type of chart we are going to show
    • height: we can simply specify how much height in px this chart is going to have
    • animation: disables all animations which may slow down the chart
    • zoomType: specifies what type of zoom we can use. ‘xy’ means that we can zoom in both directions
    • panning: true allows us to pan the chart after zooming
    • panKey: specifies which key is used for panning
  • boost: {enabled: true} : this is used to tell the chart to use the boost module
  • tooltip: the tooltip is generated by default, but by using formatter(), we can specify exactly how it will look. Here, we have access (by this keyword) for each series attribute.
  • series: this is where we put the processed array of highcharts objects
  • xAxis: it’s used for informing chart, what is the type of series, specifying title, label format, and lots of other things.

The list of values that can be set for any of those attributes can be found here.

And, honestly, this is all. By moving processing logic to the store, we won’t cause any unnecessary computations inside of the component, which will improve speed immensely. In ChartFiltrations.vue, we can add some buttons to add additional filtrations to the chart. For example, we can add buttons to specify if we want to see only one type of attribute (remember that we need to still do the filtrations inside the store). Additionally, as in this example, we have both current and voltage. As you should know, they have different units, so to make our chart more lucid, we can use multiple y axises. It can be done simply by adding a yAxis object:

  yAxis: [
    {
      title: { text: 'Current [A]' },
      gridLineWidth: 0.5,
      showEmpty: false
    },
    {
      title: { text: 'Voltage [V]' },
      gridLineWidth: 0.5,
      showEmpty: false
    }
  ]

to the chartOptions object, specifying to which y axis the dataset belongs while creating highcharts series objects:

const generateSeries = data => {
  return Object.keys(data).map(attribute => {
    return {
      type: "line",
      yAxis: attribute === "current" ? 0 : 1
    };
  });
};

Let’s see how it looks in action:

As you can see, we had two series of 100k points, and they are working smoothly. In our project, sometimes we have 1 mln points to show at once, and performance is still superb. So, we can say with certainty that Highcharts fulfills its role, for sure.

In this article, I have hopefully shown you that lots of data does not come with a need to make performance cuts. This implementation is only one of numerous approaches to this particular topic. If you have any comments or questions, I will be happy to answer them through the comments.

You can find a working example here.