loading...

Vue.js – Chapter 4: Routing

How to install and use docker on ubuntu 18.04

If you’ve built server-side web apps before, you’ll be familiar with putting some thought into the URL structure of your application.

Using a client-side router will give your SPAs the advantages of a server-side web app—namely, the ability to use the browser’s back and forward buttons to move through your app’s screens, and the ability for the user to bookmark specific screens and/or states of your app.

Vue.js has the advantage of having a library that’s not only extremely capable and well documented, but which is also the official routing solution for Vue, and is guaranteed to be maintained and kept in sync with the development of the core library.

Installing Vue Router

The router can be easily added to your app via npm (npm install vue-router), or from the Vue CLI.

For the purposes of this chapter, let’s install the router ourselves via npm and run through the steps needed to get a basic example up and running. As we go, I’ll briefly explain each of the necessary parts, which we’ll revisit in more depth later in the chapter.

If you haven’t already, use the Vue CLI to create a new project based on the “default” preset. After it has finished installing, change directory into the project’s root folder and run the following command:

npm install vue-router

Once the router library is installed, we need to configure it and add some routes to map URLs to components. Create a file called router.js inside the src folder and add the following.

src/router.js

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

We start by importing both Vue itself and Vue Router. Because the router works as a plugin, it has to be registered with Vue, which we do by calling vue.use().

Next, we create an instance of Router and pass in a configuration object. At a minimum, this needs to contain a routes key. This should be an array of object literals that define the URLs you want your app to respond to, and the components they map to. We’ll look at route configuration in more detail shortly.

You’re probably wondering about the Home component: we need to create that next.

Folder Structure

When you install Vue Router via the CLI, it generates a views folder for you. It’s a common convention for the top-level components (the ones that your app’s routes map to, the “pages”) to go inside this folder, separate from the components that represent more discrete parts of your UI. We’ll stick with this convention for our examples.

src/views/Home.vue

<template>
  <div >
    <h1 >Staff Directory</h1>
    <StaffDirectory/>
  </div>
</template>

<script>
import StaffDirectory from "../components/StaffDirectory.vue";

export default {
  name: 'HomePage',
  components: { StaffDirectory }
}
</script>

This is very similar to the code from App.vue in our example from Chapter 3. It basically serves as a “page” in our application and will be rendered when the user navigates to the root URL (that is, /).

StaffDirectory File

Don’t forget to create or copy across the StaffDirectory.vue file from the previous chapter into the src/components folder.

Our actual App component also needs amending.

src/App.vue

<template>
  <router-view></router-view>
</template>

<script>
import 'semantic-ui-css/semantic.css'

export default {
  name: "App",
};
</script>

<style>
  body {
    padding: 2em;
  }
</style>

Here we’ve replaced the component’s markup with a single tag: <router-view></router-view>. This is one of the components that Vue Router provides. We use it to specify where we want the component for each route displayed when that route is active.

Semantic UI CSS

Don’t forget to install the Semantic UI CSS module from npm:

npm i semantic-ui-css

The last thing left to do is import our route configuration into our application’s entry point, and pass it to our main Vue instance when we initialize it.

src/main.js

import Vue from "vue";
import router from "./router";
import App from "./App";

Vue.config.productionTip = false;

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

In this file, we’re importing the configured router instance we create in router.js and passing it into our main Vue instance as an option. In conjunction with having registered the router as a Vue plugin, this will make the router instance available to all components within our app.

You probably noticed that the way we’re initializing the Vue instance here is a little bit different. This is due to the fact we’re now using a module bundler in our project and an optimized build of Vue.js. Don’t worry about this too much, as setting up your projects with the Vue CLI will autogenerate this file, as we’ll see next.

Installing via Vue CLI

Installing the router via the CLI has the added bonus of adding a basic routing configuration to your app (very similar to what we created above) so you can get up and running right away.

If you’re starting a new project, choose the option to manually select features and ensure that “Router” is checked. See Chapter 2 for a more detailed run-through of using the CLI.

You can add it to an existing, CLI-created project by running vue add @vue/router from inside the project folder.

Router Config Options

The router class accepts some other useful options when you initialize it.

  • base. The base option allows you to specify a base URL that will be used for all routes. If your app lives at www.example.com/my-app/, setting the base option to my-app will automatically include this in your app’s URLs.
  • mode. This option allows you to use the router in hash mode, or history mode. (There’s a third mode, abstract, for server-side rendering.) Hash mode appends the current route to the app’s URL as a hash fragment (for example, www.example.com/#/blog), which is useful for supporting legacy browsers that don’t support HTML5 history mode.

History mode makes use of this support (now widespread in modern browsers) to provide URLs that are indistinguishable from server-side ones. The only caveat is that your app’s back end must be configured to support it by serving your app’s entry file (index.html) for every route in your application.

Example Server Configurations

The View Router docs have some handy server configuration examples.

Additional Options

The options above are the ones you’ll most commonly want to configure. There are lots of additional options detailed in the API documentation.

Routes

Creating Routes

As we saw in the basic example at the beginning of the chapter, we define our routes by passing an array of route configuration objects to the router when we instantiate it.

As a minimum, we need to supply a path (that is, a URL) and a component for each route we want to define:

import HelloWorld from './components/HelloWorld';

const routes = [
  {
    path: '/hello-world',
    component: HelloWorld
  }
];

The above route will display the HelloWorld component when the user navigates to the /hello-world URL.

There are other useful options you might want to set when creating routes.

  • name. You can supply a name for your route. This is optional, but I’d recommend it for all but the simplest of apps. Using named routes allows your URL structure to change without breaking your app, and can make it easier to navigate to programmatically when dealing with dynamic routes.
  • redirect. By entering the path of another route here, you can create redirects within your app.
  • beforeEnter. You can provide a function that will be called before the route change, allowing you to perform additional logic before continuing or canceling the navigation. Later on, we’ll look at how this can be used to make certain routes available to authenticated users only.
  • meta. You can use this option to provide an object of custom properties to be made available to the route.

Additional Options

As with the router options, there are more options you can set on routes that I haven’t gone into here. I’d recommend browsing the API documentation if you want to know more.

Route Parameters

Commonly, we’ll want to create routes that have dynamic segments, representing things such as a resource ID, or a blog post slug. Vue Router allows us to specify these dynamic segments using a parameter name prefixed with a colon.

In the case of a route for posts on a blog, we might have a route configuration something like this:

{
  name: 'blog',
  path: '/blog/:slug',
  component: BlogPost
}

Here we’ve given the route a name (we’ll see why in the next section) and specified that the segment that comes after /blog/ should be assigned to the parameter slug.

Within the app’s components, the values of any route parameters are available as this.$route.params. This could then be used to request the relevant post via Ajax.

BlogPost.vue

export default {
  name: 'BlogPost',
  data: () => ({
    title: '',
    content: ''
  }),
  created() {
    const { slug } = this.$route.params;
    fetch(`/api/posts/${slug}`)
      .then(res => res.json())
      .then(post => {
        this.title = post.title;
        this.content = post.content;
      });
  }
}

It’s a good idea to limit your use of the this.$route object to your page components and pass down any parameters via props to child components that need them. This avoids coupling your UI components to the router, making them more easily reusable.

Navigation

Changing the current route is achieved in a couple of ways: via the <router-link> component, and programmatically.

Links

While you could just use a standard <a> tag, the <router-link> component that Vue Router provides has several advantages:

  1. It automatically applies a CSS class of “active” when the URL matches the current route (the class name can be configured by setting the router’s linkActiveClass option).
  2. It takes into account whether the router is configured for hash mode or HTML5 history mode and will render the correct URL format automatically.
  3. When in history mode, it prevents the browser from reloading the page by preventing the default click action.
  4. It takes into account the base setting, if configured, and builds the URL accordingly.

The component is used in the same way you might use an <a> tag, only the URL is passed in via the to prop:

<router-link to="/hello-world">Hello, World</router-link>

In addition to a URL string, you can also pass in an object:

<router-link :to="{ path: '/hello-world' }">Hello, World</router-link>

The two examples above are functionally equivalent. Using an object becomes more useful when we want to set route or query parameters.

Route parameters

<router-link :to="{ name: 'post', params: { postId: 2 } }">
    Hello, World
</router-link>
<!-- URL: /post/2 -->

Query parameters

<router-link :to="{ name: 'posts', query: { sortby: date } }">
    Hello, World
</router-link>
<!-- URL: /posts?sortby=date -->

Programmatic Navigation

If you need to navigate around from within the code, Vue Router provides some methods for you to interact with the browser history. Each of these methods is exposed on the router instance available inside your components as this.$router.

push

The push() method takes a location object (in the same format that the <router-link> component accepts) and navigates to it. This method preserves the browser history, allowing you to return to the previous URL with the back button.

this.$router.push({ name: 'post', params: { postId: 2 } });

replace

The replace() method is almost the same as push(), except that it replaces the current entry in the browser’s history.

this.$router.replace({ name: 'post', params: { postId: 2 } });

go

The go() method allows you to move around through the browser’s history by supplying the number of steps to move as a positive or negative integer (to move forward or back, respectively).

// Return to the previous URL
this.$router.go(-1);

Navigation Guards

Navigation guards provide a way to run code at certain points in the routing process. It’s possible to supply callbacks that will run globally (that is, for all routes), or on a per-route basis.

Navigation guards are most commonly used to apply authorization checking to routes so part (or all) of your app can be restricted to authenticated users.

Global Guards

Global guards can be assigned by calling any of the methods below and passing in a function that you want to be called. Multiple functions can be assigned to each guard, and will be called in the order they’re registered in. Control will pass from one function to the next unless the navigation is canceled.

beforeEach

The beforeEach hook is called as soon as navigation is triggered to any registered route. Callbacks are passed three arguments: to, from, and next.

router.beforeEach((to, from, next) => {
  // ...
});

Both to and from are route objects that contain information about the route being navigated to and the current route, respectively. From these objects, it’s possible to inspect the path, params, query, and hash properties of the route.

The next argument is actually a callback function that your route guard must call in order to let the router know what to do next. Calling next() with no argument will execute the next route guard in the sequence (if any) or proceed with the navigation.

Navigation can be canceled by passing false, or redirected to a different route by passing a path string or location object.

beforeResolve

This hook will fire only after any beforeEach() callbacks and any of the in-component guards have run (we’ll come to these shortly). This is basically your last chance to abort the route change after all other guards have run (and passed), and the component itself has been loaded.

router.beforeResolve((to, from, next) => {
  // ...
});

Callback functions receive the same arguments as those registered to the beforeEach() guard, meaning all the same checks and outcomes are possible.

afterEach

As the name suggests, the afterEach() callbacks are run after the navigation is confirmed. Callbacks receive to and from route objects, but no next() function, as the navigation can no longer be canceled at this stage.

router.afterEach((to, from) => {
  // ...
});

The afterEach hook could be used to send data about page changes to your analytics service, for example.

Per-route guards

The per-route (and in-component) guards are useful for selectively applying logic to specific routes, but you can’t assign multiple callbacks to a hook like you can with the global guards.

beforeEnter

A beforeEnter guard can be set by assigning a function to a property of that name when adding a route.

{
  path: '/settings',
  component: SettingsPage,
  beforeEnter: (to, from, next) => {
    // ...
  }
}

The remainder of the guards we’ll cover are assigned within page components (that is, components that are loaded directly by a route) as if they were lifecycle methods.

beforeRouteEnter

This guard will be called when the route has been confirmed (that is, after any global beforeEach or per-route beforeEnter guards have run), but before the component itself has been created.

For this reason, you don’t have access to the component via the this variable. If you need to do something with the component, such as set data, you have to pass a callback to the next() function. The callback will receive the component as an argument.

export default {
  name: '...',
  props: [],
  beforeRouteEnter(to, from, next) {
    // If we need access to the component
    next(component => {
      // ...
    })
  }
}

beforeRouteUpdate

The beforeRouteUpdate guard will be called whenever the route changes but the component doesn’t. This means that if you have a route with dynamic segments, the component will be re-used and this guard will be called, allowing you to update the display.

export default {
  name: '...',
  props: [],
  beforeRouteUpdate(to, from, next) {
    // ...
  }
}

beforeRouteLeave

This method will be called on a component just before a route change that will navigate away from it. This gives you the opportunity to do any cleanup you may need.

export default {
  name: '...',
  props: [],
  beforeRouteLeave(to, from, next) {
    // ...
  }
}

Example App

I’ve created a basic example app that you can navigate around which logs out the various guard functions as they’re activated.

Note that it’s easier to see what’s being logged via the CodeSandbox console, rather than your browser’s console.

Example: Authorized Routes

To put some of this into practice, let’s look at something a lot of client-side apps typically need: protected routes. We’ll build out a simple example that will demonstrate how to configure routes with authentication checks that will redirect to a login form when a guest tries to access them.

If you want to follow along, you should start by creating a new project with the Vue CLI, not forgetting to select the option to include Vue Router.

To keep the example simple, and focus on the routing aspects, we’re going to create a mock authentication service. In a real app, this would call out to your server, or a third-party authentication service.

src/auth.js

let loggedIn = false;

export default {
  login(email, password) {
    return new Promise((resolve, reject) => {
      if (email === 'user@example.com' && password === 'password') {
        loggedIn = true;
        resolve();
      } else {
        reject();
      }
    });
  },
  logout() {
    loggedIn = false;
  },
  isAuthenticated() {
    return loggedIn;
  },
};

This authentication service provides a login() method that simply checks an email and password combination, returning a promise that’s resolved if the credentials match and rejected if they don’t.

The login method also sets the loggedIn variable to true upon successful login, which can be checked by calling the isAuthenticated() method.

Next, let’s create a login component to prompt users for their credentials.

src/views/Login.vue

<template>
  <div class="ui middle aligned center aligned grid">
    <div class="column">
      <h2 class="ui teal image header">
        <div class="content">
          Log in to your account
        </div>
      </h2>
      <form class="ui large form" @submit.prevent="onSubmit" :class="{ error }">
        <div class="ui stacked segment">
          <div class="field">
            <div class="ui left icon input">
              <i class="user icon"></i>
              <input type="text" v-model="email" placeholder="E-mail address">
            </div>
          </div>
          <div class="field">
            <div class="ui left icon input">
              <i class="lock icon"></i>
              <input type="password" v-model="password" placeholder="Password">
            </div>
          </div>
          <button type="submit" class="ui fluid large teal submit button">Login
          </button>
        </div>

        <div >Oops, we couldn't log you in!</div>

      </form>

    </div>
  </div>
</template>

<script>
import authAPI from '../auth';

export default {
  name: 'Login',
  data: () => ({
    email: null,
    password: null,
    error: false,
  }),
  methods: {
    onSubmit() {
      authAPI.login(this.email, this.password)
        .then(() => this.$router.push('/users/1'))
        .catch(() => { this.error = true; });
    },
  },
};
</script>

The login form, taken from the Semantic UI examples at semantic-ui.com, is pretty straightforward. There’s a method called onSubmit() that’s attached to the form’s submit event, which calls the auth service with the input values.

If authentication succeeds, we call this.$router.push() to navigate to our intended route. If it fails, we set an error flag in order to render a message under the login form.

Now that we have our authentication service and a way to ask for credentials, we need to secure the routes that un-authenticated users (guests) shouldn’t have access to.

src/router.js

import Vue from 'vue';
import Router from 'vue-router';
import authAPI from './auth';

import Home from './views/Home';
import Login from './views/Login';
import Users from './views/Users';

Vue.use(Router);

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: '/login',
      component: Login,
    },
    {
      name: 'user',
      path: '/users/:userId',
      component: Users,
      beforeEnter: (to, from, next) => {
        if (authAPI.isAuthenticated() === false) {
          next('/login');
        } else {
          next();
        }
      },
    },
  ],
});

export default router;

At the top of the file, we’re importing the auth service so we have a way to check the authentication status of the current user.

For the routes we want to protect, we need to assign a beforeEnter guard. The guard checks if the user is logged in and, if not, redirects to the /login route.

Let’s create the component for the Users page.

views/Users.vue

<template>
  <div>
    <h1>Users</h1>
    <p>Current route: {{ url }}</p>
    <ul>
      <li v-for="i in [1, 2, 3, 4, 5]" :key="i">
        <router-link :to="{ name: 'user', params: { userId: i } }"
          >User {{ i }}</router-link
        >
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "Users",
  data: () => ({
    url: null
  }),
  beforeRouteEnter(to, from, next) {
    next(component => (component.url = to.path));
  },
  beforeRouteUpdate(to, from, next) {
    this.url = to.path;
    next();
  }
};
</script>

If you’ve been following along with the book so far, everything here should be pretty straightforward. We have two route guards that update the page with the value of the current route path.

We also need to create a home page for the example, which will be accessible to guest users (that is, those who aren’t authenticated).

views/Home.vue

<template>
  <div >
    <h1 >Welcome</h1>
    <router-link to="/users/1">Go to Users page</router-link>
  </div>
</template>

<script>
export default {
  name: "HomePage"
};
</script>

The App.vue component will be identical to the one from the example at the beginning of the chapter, so copy that into your project, and don’t forget to install semantic-ui-css via npm.

If you’re following along with the example on your own computer, you should now be able to launch the dev server by running npm run serve and view your app at http://localhost:8080.

Online Demo

You can check out the online demo of this example on CodeSandbox.

Summary

In this chapter, I introduced Vue’s official routing solution, Vue Router, and walked you through installing and configuring it to work with a Vue application.

We looked at all the main concepts you need to understand in order to start using Vue Router in your own applications, including how to create routes and navigate between them.

We also looked at what navigation guards are, and the different kinds that are available at a global, per-route, and per-component level. Lastly, we built upon some of this knowledge to create a basic app with protected routes for authenticated users only.

Comments are closed.

loading...