An auth guard, a per-route guard, afterEach titles and an unsaved-changes leave guard
import { createRouter, createWebHistory, type RouterHistory } from "vue-router";
import { useAuth } from "./auth";
declare module "vue-router" {
interface RouteMeta {
requiresAuth?: boolean;
title?: string;
}
}
export function makeRouter(history: RouterHistory = createWebHistory()) {
const router = createRouter({
history,
routes: [
{ path: "/", component: () => import("./HomePage.vue"), meta: { title: "Home" } },
{ path: "/login", name: "login", component: () => import("./LoginPage.vue"), meta: { title: "Sign in" } },
{
path: "/settings",
component: () => import("./SettingsPage.vue"),
meta: { requiresAuth: true, title: "Settings" },
beforeEnter: (to) => {
if (to.query.tab === "danger" && !useAuth().isAdmin) return { path: "/settings" };
},
},
],
});
router.beforeEach(async (to) => {
const auth = useAuth();
if (to.meta.requiresAuth && !(await auth.check())) {
return { name: "login", query: { redirect: to.fullPath } };
}
});
router.afterEach((to, from, failure) => {
if (!failure) document.title = `${to.meta.title ?? "App"} | Acme`;
});
return router;
}
Script that prints the full guard order (Node, memory history)
import "./dom.mjs";
import { createApp, h, defineComponent, onMounted } from "vue";
import { createRouter, createMemoryHistory, RouterView, isNavigationFailure, NavigationFailureType, onBeforeRouteLeave } from "vue-router";
const log = (...a) => console.log(...a);
const Home = defineComponent({
name: "Home",
beforeRouteLeave(to, from) { log(" 1. beforeRouteLeave (Home, in-component)"); },
render: () => h("p", "home"),
});
const Account = defineComponent({
name: "Account",
beforeRouteEnter(to, from, next) {
log(" 5. beforeRouteEnter (Account, in-component, no `this` yet)");
next((vm) => log(" 8. beforeRouteEnter next(vm) callback, vm.$options.name =", vm.$options.name));
},
beforeRouteUpdate() { log(" beforeRouteUpdate (Account)"); },
mounted() { log(" 9. Account mounted"); },
render: () => h("p", "account"),
});
const routes = [
{ path: "/", component: Home },
{ path: "/account/:tab?", component: Account, beforeEnter: (to, from) => { log(" 4. beforeEnter (route config)"); } },
{ path: "/admin", component: { render: () => h("p", "admin") }, meta: { requiresAdmin: true } },
{ path: "/login", component: { render: () => h("p", "login") } },
];
const router = createRouter({ history: createMemoryHistory(), routes });
let phase = "init";
router.beforeEach((to, from) => {
if (phase === "trace") log(" 2. beforeEach (global)");
if (to.meta.requiresAdmin) return { path: "/login", query: { redirect: to.fullPath } };
});
router.beforeEach(() => { if (phase === "trace") log(" 3. beforeEach #2 (global, registration order)"); });
router.beforeResolve(() => { if (phase === "trace") log(" 6. beforeResolve (global)"); });
router.afterEach((to, from, failure) => { if (phase === "trace") log(" 7. afterEach (global) failure =", failure); });
const app = createApp({ render: () => h(RouterView) }).use(router);
await router.push("/"); app.mount("#app"); await router.isReady();
phase = "trace";
log("push('/account')");
await router.push("/account");
await new Promise(r => setTimeout(r, 10));
log("push('/account/billing') (same component, param changes)");
phase = "upd";
await router.push("/account/billing");
phase = "x";
log("--- redirect ---");
await router.push("/admin");
log("now at", router.currentRoute.value.fullPath, "redirectedFrom", router.currentRoute.value.redirectedFrom?.fullPath);
log("--- abort ---");
const off = router.beforeEach(() => false);
const f = await router.push("/");
log("push resolved with failure:", isNavigationFailure(f, NavigationFailureType.aborted), "| still at", router.currentRoute.value.fullPath);
off();
const dup = await router.push(router.currentRoute.value.fullPath);
log("same location again:", isNavigationFailure(dup, NavigationFailureType.duplicated));
log("--- throw in guard ---");
router.onError((e) => log("router.onError:", e.message));
const off2 = router.beforeEach(() => { throw new Error("guard blew up"); });
await router.push("/").catch((e) => log("push rejected:", e.message));
off2();
log("--- async guard ---");
const off3 = router.beforeEach(async (to) => { await new Promise(r => setTimeout(r, 30)); log(" async guard done for", to.fullPath); return true; });
await router.push("/"); log(" push awaited, at", router.currentRoute.value.fullPath); off3();
process.exit(0);