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.
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.
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 id="app">
<h1 class="ui center aligned header">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, /
).
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.
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 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.
The router class accepts some other useful options when you initialize it.
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.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.
The View Router docs have some handy server configuration examples.
The options above are the ones you’ll most commonly want to configure. There are lots of additional options detailed in the API documentation.
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.
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.
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.
Changing the current route is achieved in a couple of ways: via the <router-link>
component, and programmatically.
While you could just use a standard <a>
tag, the <router-link>
component that Vue Router provides has several advantages:
linkActiveClass
option).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 -->
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
.
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 } });
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 } });
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 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 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.
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.
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.
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.
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.
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.
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 => {
// ...
})
}
}
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) {
// ...
}
}
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) {
// ...
}
}
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.
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 === '[email protected]' && 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 class="ui error message">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 id="app">
<h1 class="ui center aligned header">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
.
You can check out the online demo of this example on CodeSandbox.
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.
3.144.98.13