With the advent of web applications, it became necessary to modify URLs using JavaScript. History API of the browser came to the rescue.
Because of this, all major modern frameworks allow you to programmatically control routing that synchronizes the URL with the application view.
For routing in Vue applications, you can create your own integration with the History API, but it’s better to use the official Vue library — Vue-Router
Basic things
You can start using it even with a CDN installation:
<script src="https://unpkg.com/vue-router"></script>
But we’ll start right away with the “correct” option — Vue Cli:
yarn global add @vue/cli
# OR
npm i -g @vue/cli
Let’s create a project using the VUE CLI with a basic template — Default ([Vue 2] babel, eslint):
vue create vue-router-test-app
Minimal configuration
Add a router:
yarn add vue-router
# OR
npm i --save vue-router
Let’s add the minimal router configuration to /src/main.js:
import Vue from "vue";
import App from "@/App.vue";
import VueRouter from "vue-router";
import HelloWorld from "@/components/HelloWorld";
Vue.use(VueRouter);
const routes = [
{
path: "",
component: HelloWorld,
},
];
const router = new VueRouter({
routes,
mode: "history",
});
Vue.config.productionTip = false;
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
Routes are an array, each element of which is an object where you need to specify path
and component
.
To see the changes, you need to display the router component — routerView
which is responsible for displaying. To do this, let's change /src/App.vue
:
<template>
<div id="app">
<router-view />
</div>
</template>
Now, go to http://localhost:8080/. We will see a page with a “/” route, where the HelloWorld.vue
component is displayed, instead of the tag router-view
that we wrote in App.vue
.
Path hierarchy
Let’s add a route to main.js
(array routes):
const routes = [
{
path: "",
component: HelloWorld,
},
{
path: "/board",
component: {
render: (h) => h("div", ["Board Page"]),
},
},
];
Let’s go to the address http://localhost:8080/board. We will see the second page displaying the render function.
Route Props
Let’s fix the child route for the “/” board route in main.js. For child components, you need to specify where in the parent component to display the child components router-view
. In our case, this is in the render function:
import Board from "@/components/Board";
const routes = [
{
path: "",
component: HelloWorld,
},
{
path: "/board",
component: {
render: (h) => h("div", ["Board Page", h("router-view")]),
},
children: [
{
path: '/board/:id',
component: Board,
}
]
},
];
Let me remind you that the render function in the template view will look like this:
<template>
<div>
Board Page
<router-view />
</div>
</template>
Let’s create a /src/components/Board.vue
component with content:
<template>
<div>Board with prop id: {{ id }}</div>
</template>
<script>
export default {
computed: {
id() {
return this.$route.params.id;
},
},
};
</script>
Let’s go to the address http://localhost: 8080/board/21 and see the parent and child components Board
passing the parameter id
equal to 21.
Route parameters are available in the by component this.$route.params
.
If we want to more explicitly display the dependence of the component on the input parameters, we use the setting props: true
when configuring the route:
children: [
{
path: '/board/:id',
component: Board,
props: true,
}
]
And in the /src/components/Board.vue
component, accept idas an input parameter of the component:
<template>
<div>Board with prop id: {{ id }}</div>
</template>
<script>
export default {
props: {
id: {
type: String,
default: null,
},
},
};
</script>
Route meta
const routes = [
{
path: "",
component: HelloWorld,
meta: {
dataInMeta: "test",
},
},
....
]
We can now access the route metadata from the HelloWorld.vue
component as follows: this.$route.meta.dataInMeta
.
Deeper Vue.js Routing (nested children)
You can go deeper into child components (up to server limits).
Let’s make a child route for the child route:
const routes = [
{
path: "",
component: HelloWorld,
},
{
path: "/board",
component: {
render: (h) => h("div", ["Board Page", h("router-view")]),
},
children: [
{
path: "/board/:id",
component: Board,
props: true,
children: [
{
path: "child",
component: {
render: function(h) {
return h("div", ["I'm Child with prop", this.propToChild]);
},
props: {
propToChild: {
type: Number,
required: true,
default: null,
},
},
},
},
],
},
],
},
];
The render function is now written as a regular function since you need a component context:
<template>
<div>
Board with prop id: {{ id }}
<router-view :prop-to-child="parseInt(id)" />
</div>
</template>
<script>
export default {
props: {
id: {
type: String,
default: null,
},
},
};
</script>
We pass the parameters to the child component of the child component through the router-view
component like a normal component. It sounds complicated but intuitive. And so, we lower the props in the child — child of the child: <router-view :prop-to-child="parseInt(id)" />
Explanation of Path
The view path: "child"
means that we refer to the parent's path and continue its path:{parent-route}/child
Any other level of the route can be referenced from the child component:
children: [
{
path: "/first-level",
....
}
]
This entry processes a page with the address: http://localhost:8080/first-level.
Wider Vue.js Routing (multiple router-views)
Can be used multiple router-view
in 1 component. To do this, in the routes configuration, we write instead of component - components, which takes an object, where the key is the name
attribute router-view
. If you specify the key "default", then such a component will be displayed if it is router-view
unnamed (without an attribute name
).
/src/main.js
:
const routes = [
{
path: "",
component: HelloWorld,
},
{
path: "/board",
component: {
render: (h) => h("div", ["Board Page", h("router-view")]),
},
children: [
{
path: "/board/:id",
component: Board,
props: true,
children: [
{
path: "child",
components: {
default: { render: (h) => h("div", ["I'm Default"]) },
user: { render: (h) => h("div", ["I'm User"]) },
guest: { render: (h) => h("div", ["I'm Guest"]) },
},
},
],
},
],
},
];
/components/Board.vue
:
<template>
<div>
Board with prop id: {{ id }}
<div>
<label for="is-user">
Is User?
<input v-model="isUser" id="is-user" type="checkbox" />
</label>
<router-view :prop-to-child="parseInt(id)" />
<router-view v-if="isUser" name="user" />
<router-view v-else name="guest" />
</div>
</div>
</template>
<script>
export default {
props: {
id: {
type: String,
default: null,
},
},
data() {
return {
isUser: false,
};
},
};
</script>
Let’s go to the address: http://localhost:8080/board/23/child
and see a small interactive with switching active router-view
s.
404 error page
To create an error page, just put the following construction at the end of the list of routes:
{
path: "*",
component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},
Now, when following a nonexistent path (for example, ** http://localhost:8080/mistake **), an error component will be displayed.
It is better to write in this form:
{
path: "/page-not-found",
alias: '*',
component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},
Now we have an error page where we can redirect users with a clear conscience (what if one day we need to do this).
Route protection
Route protection is performed using route metadata and a beforeEach
router hook:
import Vue from "vue";
import App from "@/App.vue";
import VueRouter from "vue-router";
import HelloWorld from "@/components/HelloWorld";
import Board from "@/components/Board";
Vue.use(VueRouter);
const routes = [
{
path: "",
component: HelloWorld,
},
{
path: "/board",
component: {
render: (h) => h("div", ["Board Page", h("router-view")]),
},
meta: {
requiresAuth: true,
},
children: [
{
path: "/board/:id",
component: Board,
props: true,
children: [
{
path: "child",
components: {
default: { render: (h) => h("div", ["I'm Default"]) },
user: { render: (h) => h("div", ["I'm User"]) },
guest: { render: (h) => h("div", ["I'm Guest"]) },
},
},
],
},
],
},
{
path: "/auth-required",
component: { render: (h) => h("div", ["Auth required!"]) },
},
{
path: "/*",
component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},
];
const router = new VueRouter({
routes,
mode: "history",
});
const isAuthenticated = () => false;
router.beforeEach((to, from, next) => {
if (to.matched.some((route) => route.meta?.requiresAuth)) {
if (isAuthenticated()) {
next();
} else {
next("/auth-required");
}
} else {
next();
}
});
Vue.config.productionTip = false;
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
Now, when trying to access a page that requires authorization, we will be redirected to the /auth-required
page.
Navigation between routes
Software navigation
Programmatic navigation can be called from anywhere in your application like this:
$router.push('/dash/23/child')
If we want to pass parameters, we need to use a different approach based on the use of route names.
Let’s specify the name of the route /board/:id
:
...
children: [
{
path: "/board/:id",
name: 'board',
component: Board,
props: true,
children: [
....
Now we can pass parameters:
$router.push({ name: 'board', params: { id: 100500 }})
We will get an error «Invalid prop: type check failed for prop «id». Expected String with value «100500», got Number with value 100500».
The reason is that url-
it is always a data type String, and we passed it programmatically id
with a type Number
. The fix is simple: we list the possible data types in the component.
components/Board.vue
:
props: {
id: {
type: [String, Number],
default: null,
},
},
RouterLink component
The component routerLink
allows you to create links within the site, which are converted into "native" browser links (tag <а>
):
<router-link to='/dash/23/child'> Link </router-link>
Classes can be automatically added to such links:
-
router-link-exact-active
- exact match; -
router-link-active
- partial (the child component specified in theto
route attribute is active).
In order not to display the active parent class, it is enough to write the attribute exact:
<router-link to='/dash/23/child' exact> Link </router-link>
We can override the element we create:
<router-link tag="button" to='/dash'> Button </router-link>
Unfortunately, in this case, the classes are not assigned.
We can also pass an object:
<router-link :to="{ path: '/dash/23' "> Link </router-link>
<router-link :to="{ name: 'board', params: { id: 123 } }"> Link </router-link>
Best practics
We will devote this section to refactoring what we wrote above.
Create a folder structure for the router:
src/router/router.js
src/router/routes.js
Let’s transfer everything related to router settings to router.js
:
mport Vue from "vue";
import VueRouter from "vue-router";
import routes from "/routes";
Vue.use(VueRouter);
const router = new VueRouter({
routes,
mode: "history",
base: process.env.BASE_URL,
});
const isAuthenticated = () => true;
router.beforeEach((to, from, next) => {
if (to.matched.some((route) => route.meta?.requiresAuth)) {
if (isAuthenticated()) {
next();
} else {
next("/auth-required");
}
} else {
next();
}
});
export default router;
Let’s transfer routes.js
everything related to route settings.
And immediately replace the imports with dynamic ones.
If you already have a lot of routes assigned, manual changes can be time-consuming. The regular will help:
^import (\w+) from (".+")$
replaced by
const $1 = () => import(/* webpackChunkName: "$1" */ $2)
Now in Chrome Dev Tools, in the Network tab, you will see when-which component is loaded from the network, and earlier all routes were loaded immediately in 1 mega-bundle.
/src/router/routes.js
:
const HelloWorld = () => import(/* webpackChunkName: "HelloWorld" */ "@/components/HelloWorld")
const Board = () => import(/* webpackChunkName: "Board" */ "@/components/Board")
const routes = [
{
path: "",
component: HelloWorld,
},
{
path: "/board",
component: {
render: (h) => h("div", ["Board Page", h("router-view")]),
},
meta: {
requiresAuth: true,
},
children: [
{
path: "/board/:id",
name: "board",
component: Board,
props: true,
children: [
{
path: "child",
components: {
default: { render: (h) => h("div", ["I'm Default"]) },
user: { render: (h) => h("div", ["I'm User"]) },
guest: { render: (h) => h("div", ["I'm Guest"]) },
},
},
],
},
],
},
{
path: "/auth-required",
component: { render: (h) => h("div", ["Auth required!"]) },
},
{
path: "/*",
component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},
];
export default routes;
Advanced tricks in Vue.s Routing
By “advanced” is meant the “pleasantness” of using them. Such techniques include, for example, topics such as:
- division of rights by access levels;
- animation of transitions between pages; l+ ading indication when switching between routes;
- changing titles when switching between routes;
- smooth scrolling across the page when moving back;
- etc. So, everything in order.
Splitting rights by access levels
There is a situation when users have more than two states: not only authorization but others as well. For example, a paid subscription. From now on, we are thinking about an unlimited level of separation of rights. This is done in just a couple of dozen lines of code, but for brevity, convenience and not to reinvent the wheel, we will use a ready-made library. Let’s install it:
yarn add vue-router-middleware-plugin
Let’s create special middleware files for checking user rights:
router/middleware/authMiddleware.js
:
const isLoggedIn = () => !!window.localStorage.getItem("logged-in")
const authMiddleware = async ({ /* to, from to,*/ redirect }) => {
if (!isLoggedIn()) {
redirect({
name: "login",
});
}
};
export default authMiddleware;
router/middleware/guestMiddleware.js
:
const isLoggedIn = () => !!window.localStorage.getItem("logged-in");
const guestMiddleware = async ({ /* to, from to,*/ redirect }) => {
if (isLoggedIn()) {
redirect({ name: "main" });
}
};
export default guestMiddleware;
router/middleware/subscribersMiddleware.js
:
const isSubscribed = () => Promise.resolve(!!window.localStorage.getItem("has-license"))
const subscribersMiddleware = async ({ /* to, from, */ redirect }) => {
if (!await isSubscribed()) {
console.log("isn't subscribed, redirect to license")
redirect({ name: 'license' })
}
}
export default subscribersMiddleware
The last listing shows an example of an asynchronous check, which means that you can access the store’s actions and make requests to the server.
Now let’s put an authorization check on all routes, and then we’ll make exceptions for some routes:
/src/router/router.js
:
import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes";
import MiddlewarePlugin from "vue-router-middleware-plugin";
import authMiddleware from "./middleware/authMiddleware";
Vue.use(VueRouter);
const router = new VueRouter({
routes,
mode: "history",
base: process.env.BASE_URL,
});
Vue.use(MiddlewarePlugin, {
router,
middleware: [authMiddleware],
});
export default router;
Now let’s deal with specific routes.
Let’s work on the architecture of our application to make it more predictable. Let’s make a separate Auth.vue template and put it in pages, and the components that are used there, i.e. in the /auth
section, put components in the appropriate section.
So a convenient structure is obtained:
pages
--Auth.vue
components
-- auth
---- Login.vue
---- Register.vue
---- Forgot.vue
Let’s create a helper function to generate such routes genAuthRoutes
.
/src/router/routes.js
:
import guestMiddleware from "./middleware/guestMiddleware";
import authMiddleware from "./middleware/authMiddleware";
import subscribersMiddleware from "./middleware/subscribersMiddleware";
const MainBoard = () =>
import(/* webpackChunkName: "MainBoard" */ "@/pages/MainBoard");
const BoardComponent = () =>
import(
/* webpackChunkName: "BoardComponent" */ "@/components/board/BoardComponent"
);
const clearAndUpper = (text) => text.replace(/-/, "").toUpperCase();
const toPascalCase = (text) => text.replace(/(^\w|-\w)/g, clearAndUpper);
const genAuthRoutes = ({ parent, tabs = [] }) => ({
path: `/${parent}`,
name: parent,
component: () => import(/* webpackChunkName: "auth" */ "@/pages/Auth"),
redirect: { name: tabs[0] },
children: tabs.map((tab) => {
const tabPascalCase = toPascalCase(tab);
return {
path: tab,
name: tab,
component: () =>
import(
/* webpackChunkName: "[request]" */ `@/components/${parent}/${tabPascalCase}`
),
meta: {
middleware: {
ignore: [authMiddleware],
attach: [guestMiddleware],
},
},
};
}),
});
const routes = [
genAuthRoutes({ parent: "auth", tabs: ["login", "register", "forgot"] }),
{
path: "/",
name: "main",
component: MainBoard,
children: [
{
path: "/board",
name: "board",
component: {
render: (h) => h("div", ["Board Page", h("router-view")]),
},
children: [
{
path: "/board/:id",
name: "board-child",
component: BoardComponent,
props: true,
children: [
{
path: "child",
components: {
default: { render: (h) => h("div", ["I'm Default"]) },
user: { render: (h) => h("div", ["I'm User"]) },
guest: { render: (h) => h("div", ["I'm Guest"]) },
},
meta: {
middleware: {
attach: [subscribersMiddleware],
},
},
},
],
},
],
},
{
path: "/license",
name: "license",
component: {
render: (h) => h("div", ["License Page"]),
},
},
],
},
{
path: "/auth-required",
name: "auth-required",
component: { render: (h) => h("div", ["Auth required!"]) },
meta: {
middleware: {
ignore: [authMiddleware],
},
},
},
{
path: "/*",
component: { render: (h) => h("div", ["404! Page Not Found!"]) },
meta: {
middleware: {
ignore: [authMiddleware],
},
},
},
];
export default routes;
We remove the global authorization check in the property ignoreand add another check in the attachobject property meta.middleware
:
```middleware: {
ignore: [authMiddleware],
attach: [guestMiddleware],
}
Let’s create the components:
+ /src/components/auth/Login.vue;
+ /src/components/auth/Register.vue;
+ /src/components/auth/Forgot.vue,
with a typical template:
```html
<template>
<div>
Forgot Page
</div>
</template>
We’ll also refactor the page Board
, let's call it MainBoard
/src/pages/MainBoard.vue
:
<template>
<div>
<h1>Main Board Page</h1>
<router-view />
</div>
</template>
Accordingly, we add components to the appropriate category in components:
/src/components/board/BoardComponent.vue
:
<template>
<div>
Board with prop id: {{ id }}
<div>
<label for="is-user">
Is User?
<input v-model="isUser" id="is-user" type="checkbox" />
</label>
<router-view :prop-to-child="parseInt(id)" />
<router-view v-if="isUser" name="user" />
<router-view v-else name="guest" />
</div>
</div>
</template>
<script>
export default {
props: {
id: {
type: [String, Number],
default: null,
},
},
data() {
return {
isUser: false,
};
},
};
</script>
It remains to refactor the main component — /src/App.vue
:
<template>
<div id="app">
<div class="links">
<router-link :to="{ name: 'register' }">Register</router-link>
<router-link :to="{ name: 'login' }">Login</router-link>
<router-link :to="{ name: 'forgot' }">Forgot</router-link>
<template v-if="loggedIn">
<router-link :to="{ name: 'license' }">License</router-link>
<router-link :to="{ name: 'board' }">Board</router-link>
<router-link :to="{ name: 'board-child', params: { id: 33 } }"
>Board:33</router-link
>
<router-link :to="{ path: '/board/33/child' }"
>Board:33/child</router-link
>
<router-link :to="{ path: '/404' }">404</router-link>
</template>
<label for="logged-in"
>Logged In
<input type="checkbox" id="logged-in" v-model="loggedIn" />
</label>
<label for="has-license"
>Has License
<input type="checkbox" id="has-license" v-model="hasLicense" />
</label>
</div>
<router-view />
</div>
</template>
<script>
export default {
data() {
return {
loggedIn: !!window.localStorage.getItem("logged-in"),
hasLicense: !!window.localStorage.getItem("has-license"),
};
},
watch: {
loggedIn(e) {
window.localStorage.setItem("logged-in", e ? true : "");
},
hasLicense(e) {
window.localStorage.setItem("has-license", e ? true : "");
},
},
};
</script>
<style scoped>
.links > * {
margin: 1em;
}
</style>
Now, uncheck “Logged In” and try to follow the route http: //localhost:8080/board. We will be immediately redirected to the “auth-required” page.
Check “Logged In”, uncheck “Has License” and navigate to http://localhost: 8080/board/33/child. We will be taken to the license page, however, if you uncheck “Logged In” and refresh the page, then we will go back to the “auth-required” page.
Now let’s check if it is possible to enter the authorization page when the user has already been authorized. Check the box “Logged In” and go to http://localhost:8080/auth/register. We will be redirected to the main page.
Read More
If you found this article helpful, click the💚 or 👏 button below or share the article on Facebook so your friends can benefit from it too.
Top comments (1)
Wow thanks