Use Vue.js Data Binding Options for Reactive Applications

Publikováno: 17.6.2019

Vue.js is known as a "progressive framework for building user interfaces". There's a lot to unpack in this simple statement. It's easy to get started with Vue.js, with a minimal feature set, and th...

Celý článek

Vue.js is known as a "progressive framework for building user interfaces". There's a lot to unpack in this simple statement. It's easy to get started with Vue.js, with a minimal feature set, and then layer in more of the framework as you need it.

Unike React, it has full support for the MVC (Model View Controller) pattern out-of-the-box.

It's easier to use and grow with than Angular.

And, if you couldn't tell, I'm a little biased.

Vue.js has full support for ECMA 6 (sometimes referred to as ES6 or ES2015). This means it's now very easy to make your applications modular as well as being able to support modern syntax, like: import.

Vue.js has a lot of options for managing reactive data-binding in your application. This is the ability for views to automatically update when models (data) change.

In this post, you'll look at three different approaches, each with their own pros and cons. For each of the three approaches, you'll work with the same application: a progress bar that you can control with buttons. Then, you'll dig deeper into the last option with a more complex code example.

The application also uses the BootstrapVue project which gives us a set of easy tags and components to work with for demonstration. You'll make extensive use of the progress bar component.

Later, you'll make use of the Vuex library for formal management of data stores. You'll see how we can use these data stores to manage login and logout with Okta. First, let's look at: Why use Okta?

Why Use Okta for Authentication?

While the example app in this post is focused on data binding, you're going to be building a real-world application. The application includes authentication using the OpenID Connect standard in conjunction with Okta and stores the results of the authentication in the advanced data store for Vue.js. Okta makes identity management easier, more secure, and more scalable than what you’re used to. Okta is an API service that allows you to create, edit, and securely store user accounts and user account data, and connect them with one or more applications. As a developer, I know that I need authentication. But, I’ve seen enough horror stories from breaches over the years that I am happy to not handle credentials directly. Our API enables you to:

To get started on this tutorial, register for a forever-free developer account, or sign in if you already have one. When you’re done, come back to learn more about building a secure SPA app with Vue.js and Vuex.

Use a Global Data Object for Simple Requirements

Using a global data object is straightforward and functional. It's very accessible and the easiest of the approaches you'll look at. It's also the most fragile approach and requires duplicated code.

Let's start by building and running the application. Clone the vue-data-binding-approaches GitHub project. Switch to the project folder, and run:

cd basic
npm install
npm run serve

This runs a local instance of the application. Launch your browser and navigate to: http://localhost:8080.

For this section of the post, you'll be using the Global tab. Click Advance progress bar and you'll see the progress bar move. Click the Two tab, and you should see the progress bar at the same point. The progress bars on tabs One and Two are kept in sync automatically through a global data object.

Let's look at the code that backs the Global tab:

In the main.js file, I define the global data object:

export const globalData = {
  state: {
    max: 50,
    score: 0
  }
}

Notice that within the globalData object, there's a state object. Within state are the actual properties we want to make sure are reactive. Due to the limitations of modern JavaScript, Vue.js cannot detect property addition or deletion. As long as we preserve globalData.state, Vue.js will be able to keep variables inside it reactive.

Let's take a look at the parts of the basic/src/components/data-binding-global/One.vue template which manipulates the progress bar and keeps the data in sync.

Starting with the <script> section first, you can see that the globalData object defined in main.js is imported into this template.

import { globalData } from '../../main'

The data function binds the globalData object to a local variable in this template:

data() {
    return {
        scoreState: globalData.state
    }
}

The advance and reset functions manipulate the values in the globalData object. This is done by using the local template reference, which "points" to the object within our data structure. This is what preserves the reactive nature of the data in the template and why we need to nest the data properties in globalData.

advance: function () {
    if (this.scoreState.score < this.scoreState.max) {
        this.scoreState.score += 10
    }
},
reset: function () {
    this.scoreState.score = 0
}

Tying it all together is the template section. Here's the progress bar:

<b-progress :max="scoreState.max" class="big-progress" show-progress>
    <b-progress-bar :value="scoreState.score"/>
</b-progress>

In the <b-progress> tag, the value for max is bound to the local template data object: scoreState.max. In the <b-progress-bar> tag, the value for value is bound to the local template data object: scoreState.score.

Finally, the template has buttons that when clicked call the advance and reset functions respectively to allow you to manipulate the progress bar.

basic/src/components/data-binding-global/Two.vue is almost an exact replica of One.vue. And, herein lies the issue with this approach: lots of repeated code.

Try out the Global tab on the app and you should see that however far you advance the One tab within it, when you click the Two tab, the progress bar will be at the same location. This is proof that our global reactive data binding is working.

We can improve on this code using the Storage Pattern to centralize initialization and logic code.

Use the Storage Pattern to Centralize Data Update Logic

Click on the Storage Pattern of the app. You can use the controls and switch between the One and Two tabs. It looks just the same as the previous example. You'll see the difference as we dig into the code.

In this version of the code, you use a centralized data store which includes not only the data object and initial states but also all the business logic.

Take a look at the basic/src/model/scoreStore.js:

export default {
    state: {
        max: 50,
        score: 0
    },
    reset: function () {
        this.state.score = 0
    },
    score: function (score) {
        if (this.state.score < this.state.max) {
            this.state.score += score
        }
    },
    bumpScore: function () {
        this.score(10)
    }
}

There's still a state object that contains the internals of the data we want to be reactive in the application. There's also reset, score, and bumpScore functions containing the business logic that was repeated across components in the previous example.

Now, take a look at the script section of basic/src/components/data-binding-storage/One.vue:

import scoreStore from '../../model/scoreStore'

export default {
    name: 'One',
    data() {
        return {
            scoreState: scoreStore.state
        }
    },
    methods: {
        advance: () => scoreStore.bumpScore(),
        reset: () => scoreStore.reset()
    }
}

the data() function is very similar to what we saw before. You bind the data state from the central model store.

The local functions in the methods section simply refer to functions from the central store.

There's a great reduction in the repeated code in the components and any additional logic can be added to the central store.

This approach is robust and functional for simple projects. There are some shortcomings, however. The central store has no record of which component changed its state. Further, we want to evolve the approach where components can't directly change state, but rather trigger events that notify the store to make changes in an orderly manner. For more complex projects, it's useful to have a more formal data binding paradigm.

This is where Vuex comes in.

Use Vuex for Modern Data Binding

Vuex is inspired by other modern state management frameworks, like flux. It accomplishes two primary goals:

  1. A centralized, reactive data store
  2. Components cannot directly change state

Look at basic/src/model/scoreStoreVuex.js:

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

Vue.use(Vuex)

export default
 new Vuex.Store({
    state: {
        max: 50,         score: 0
    },
    mutations: {
        bumpScore (state) {
            if (state.score < 50) {
                state.score += 10
            }
        },
        reset (state) {
            state.score = 0
        }
    }
  })

This time, you're instantiating a Vuex object. Similar to the storage pattern from earlier, we define a state object in the Vuex.Store. What's different here, is that there's a mutations section. Each function in this section receives state as a parameter. These functions are not called directly.

You can see how changes to the datastore state are made by looking at basic/src/components/data-binding-vuex/One.vue. Take a look at the script section:

import scoreStoreVuex from '../../model/scoreStoreVuex'

export default {
    name: 'One',
    data() {
        return {
            scoreState: scoreStoreVuex.state
        }
    },
    methods: {
        advance: () => scoreStoreVuex.commit('bumpScore'),
        reset: () => scoreStoreVuex.commit('reset')
    }
}

Notice that the advance and reset functions call the commit function on scoreStoreVuex. The commit function takes a text parameter which is the name of one of the mutations we defined in the Vuex store.

Once again, you can see that our progress meter is kept in sync across the One and Two views.

The progress meter is a very simple example. In the next section, we'll examine more complex uses of Vuex.

Advanced Data Binding with Vuex

Aside from ensuring that data elements cannot be directly changed, Vuex has a number of other features that adds to its usefulness. In this section, you'll examine store injection, a helper for computed fields and managing more complex data objects, like arrays and javascript objects.

Vuex Store Injection

In this section, you use the vuex-advanced folder in the okta-vuejs-data-binding-example project.

At the top level of your app, you can inject the Vuex store. This will make it available to all components in the project without needing to explicitly import it.

It looks like this:

const store = new Vuex.Store({
...
})

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

Now, in any component, you need only refer to: this.$store to work with the Vuex data store.

Vuex Computed Fields

Computed fields are a key feature of Vue.js in general. Vuex provides advanced functionality to hook into the store to capture changes and make it easy to update the view automatically.

The mapState is a helper wrapper that can be used for computed fields. It looks like this:

import { mapState } from 'vuex';

computed: mapState([
    'ary', 'obj'
])

This is a shorthand for referencing state.ary and state.obj from the Vuex store. With this setup, you can then reference the computed value in your template:

<h1>{{ary}}</h1>

Managing Arrays and Objects with Vuex

In the basic section of this post, you were working with very simple values, like an integer for the progress meter.

Let's take a look at how to manage arrays and objects with Vue.js and Vuex.

Because of the reactive nature of the data in the Vuex store, it's important that you don't delete or replace more complex objects and arrays. Doing so would break reactivity and updates would no longer be shown in views.

In the vuex-advanced application, you can add and delete elements from both arrays and objects.

Take a look at the add and del functions in the Vuex store:

const store = new Vuex.Store({
  state: {
    ary: [],
    obj: {}
  },
  mutations: {
    addAry: function (state, elem) {
      state.ary.push(elem)
    },
    delAry: function (state) {
      state.ary.splice(-1, 1)
    },
    addObj: function (state, elem) {
      Vue.set(state.obj, elem.key, elem.value)
    },
    delObj: function (state, name) {
      Vue.delete(state.obj, name);
    }
  }
})

For arrays, use the push function to add elements and the `splice` function to remove elements.

For objects, use the Vue.set to add elements and `Vue.delete` to remove elements.

In either case, the original object is never destroyed, preserving reactivity.

Use Okta and Vuex for Easy Login with OpenID Connect

Now that you've seen various approaches for data binding with Vue.js, let's take a look at a practical application of Vuex.

In this section, you'll develop a small, Single Page App (SPA) that integrates with Okta for authentication.

The app makes use of OpenID Connect, so let's start with a quick overview of this standard for authentication and identity management.

A Five-Minute Overview of OpenID Connect

OpenID Connect is an identity and authentication layer that rides on top of OAuth 2.0. In addition to “knowing” who you are, you can use OIDC for Single Sign-On.

OIDC is built for web applications as well as native and mobile apps. It’s a modern approach to authentication that was developed by Microsoft, Google and others. It supports delegated authentication. This means that I can provide my credentials to my authentication provider of choice (like Okta) and then my custom application (like a Vue.js app) gets an assertion in the form of an ID Token to prove that I successfully authenticated.

OpenID Connect uses “flows” to accomplish delegated authentication. This is simply the steps taken to get from an unauthenticated state in the application to an authenticated state. For the SPA app, you'll use the implicit flow for obtaining an ID Token. Here's what the interaction looks like:

When you click the Login button in the app, you're redirected to Okta to authenticate. This has the advantage of your app not being responsible for handling credentials. Once you've authenticated at Okta, you're redirected back to the app with an ID Token. The ID Token is a cryptographically signed JWT that carries identity information in its payload. The app can then extract user information from the token. Additionally, the app uses Vuex to store the ID Token, which can be used later to log out. To learn more about OAuth 2.0 and OIDC, check out these blog posts:

Set Up Okta for the SPA App

Head on over to https://developer.okta.com to create an Okta org.

Login to your Okta org. Click Applications on the top menu. Click Add Application. Click Single-Page App and click Next.

Give your app a name. Change the Login redirect URIs field tohttp://localhost:8080. Click Done.

There's one more thing we need to configure in order to support logout.

Click Edit. Uncheck Allow Access Token with implicit grant type (we will only be using the ID Token in this example). Click Add URI next to Logout redirect URIs. Enter: http://localhost:8080/.

Use Vuex and the Okta Auth Javascript Library

The okta-auth-js library includes support for OpenID Connect.

You can add it to your Vue.js project like so:

npm install @okta/okta-auth-js --save

Just like before, you configure Vuex in main.js

const store = new Vuex.Store({
  state: {
    user: {},
    idToken: ''
  },
  mutations: {
    setUser: function (state, elem) {
      Vue.set(state.user, elem.key, elem.value);
    },
    setIdToken: function (state, value) {
      state.idToken = value;
    }
  }
});

In this case, the data store keeps information about the user and the raw JWT in idToken.

The Home.vue file is the only component in this app. Okta will redirect back to this component both when you log in and when you log out.

Here's the code to import the okta-auth-js library and set up some constants:

import OktaAuth from '@okta/okta-auth-js';

const ISSUER = 'https://{yourOktaDomain}/oauth2/default';
const CLIENT_ID = '{yourClientId}';
const REDIRECT_URI = 'http://localhost:8080';
var authClient;

The created function is run when the component is first created. In this function, the authClient is set up:

created() {
  authClient = new OktaAuth({
    issuer: ISSUER,
    clientId: CLIENT_ID,
    redirectUri: REDIRECT_URI
  });
}

The template is very simple. It shows the user information (which will be empty if you're not logged in). It shows a Login button if you're not currently authenticated and a Logout button if you are already authenticated.

<template>
  <div>
    <h1>Data Binding with Vue.js</h1>
    <h3>User Info:</h3>
    <div class="div-centered">
      <codemirror :value="userStr" :options="cmOptions"></codemirror>
    </div>
    <b-button v-if="!user.claims" @click="login" variant="primary" class="m-1">
        Login</b-button>
    <b-button v-if="user.claims" @click="logout" variant="danger" class="m-1">
        Logout</b-button>
  </div>
</template>

The app uses CodeMirror to display the JSON representing the user information formatted, indented and with line numbers.

Notice :value="userStr" in the <codemirror> tag. If you examine the computed section of the script, you can see how userStr is computed:

userStr() {
  return JSON.stringify(this.$store.state.user, null, '\t')
}

JSON.stringify is used so that codemirror can display the information properly. The important bit is: this.$store.state.user. This retrieves the bound value from the Vuex store.

When you first browse over to the app at http://localhost:8080, the view is pretty sparse:

When you click Login, you're redirected over to Okta:

Here's the login function in the methods section:

login() {
  authClient.token.getWithRedirect({
    responseType: 'id_token',
    scopes: ['openid', 'email', 'profile']
  })
}

The options passed into getWithRedirect ensure that you get back an id_token as well as specifying some default scopes in the request.

After you authenticate, Okta redirects back to http://localhost:8080. The mounted function is executed once the component is completely loaded and ready for action.

async mounted() {
  // check for tokens from redirect
  if (location.hash) {
    var tokenInfo = await authClient.token.parseFromUrl();
    this.$store.commit(
        'setUser', {key: 'claims', value: tokenInfo.claims}
    );
    this.$store.commit('setIdToken', tokenInfo.idToken);
  }
}

The redirect from Okta includes the id_token value in the URL in the fragment section. It looks something like this:


http://localhost:8080/#id_token=eyJraWQiOiI3bFV0aGJyR2hWVmxVT2RzVldwWFQwaWdyUEVGOEl6ZUtvdW53ckZocWxzIiwiYWxnIjoiUlMyNTYifQ...

The mounted function first checks to see if there's a hash (#) in the location URL. Note: When you first browse to the app, there is no hash in the URL, so the if statement will not be entered.

authClient.token.parseFromUrl() grabs the id_token from the url fragment, validates the cryptographic signature and extracts the json payload (the claims) from it.

The next two lines save the parsed claims as well as the raw JWT in the Vuex store. As we saw before, the code is using this.$store.commit to take advantage of the mutations defined in the store.

Because of the data binding and computed values we setup earlier, the user info is now displayed in the component.

Now when you click Logout, the app uses the information in the Vuex store to properly execute the logout operation and destroy your session with Okta.

To log out with OIDC, you make a GET request of a /logout endpoint. You pass along the ID Token (as the raw JWT), as well as a redirect URI so that Okta can redirect back to the app after logout is complete. This is all set up in the logout function in the SPA app:

logout() {
  window.location.href = 
    ISSUER + '/v1/logout?id_token_hint=' + this.$store.state.idToken +
    '&post_logout_redirect_uri=' + REDIRECT_URI
}

This closes the loop on our SPA app, its use of the okta-auth-js library in conjunction with Vuex to manage data stores and how that data is bound to the component.

Pick the Optimal Vue.js Data Binding Approach

All the code for this post can be found on Github.

In the simplest cases, a global data store may suit your needs. Even for more complex applications, the storage pattern may suffice. I've written a number of Vue.js applications that are in production that use the storage pattern. This includes the online version of the Zork game that teaches you a little about OAuth 2.0.

Vuex offers a tradeoff between slightly more complex code and a high degree of stability and testability. Vuex makes it easy to inject the data store into your components using this.$store and ensures that data cannot be directly updated by components.

As is almost always the case, you'll need to pick the approach that makes the most sense for your use-case.

Learn More About Vue.js and Secure User Management

Okta's written a Vue.js integration that makes integrating with Okta for secure auth a snap. It's part of our open-source javascript OpenID Connect library. You can go directly to the Vue.js integration as well.

At Okta, we say: friends don't let friends build auth! If you're working on a project that requires secure, reliable authentication and authorization, get a free developer account from Okta.

Here are some more Vue.js posts that might interest you:

Check out the Okta Developer YouTube channel. You can follow us on social @oktadev

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace