Skip to content

vue-router

  • <RouterLink /> 链接到 to 属性指定的路由
  • <RouterView /> 路由组件的容器
  • useRoute() 获取路由对象
  • useRouter() 获取路由器对象
import {
createRouter,
createWebHistory,
type RouteRecordRaw,
} from "vue-router";
import LoginView from "@/views/LoginView.vue";
const routes: Array<RouteRecordRaw> = [
{
path: "/",
// 同步导入的路由组件, 合并打包
component: LoginView,
},
{
path: "/register",
// 异步导入的路由组件, 分开打包
component: () => import("@/views/RegisterView.vue"),
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
export default router;

<RouterLink to="/where" /><a href="/where"></a> 的区别

  1. <RouterLink /> 在 hash 模式和 history 模式下的行为相同
  2. <RouterLink /> 会阻止 <a> 标签点击事件的默认行为, 不会重新加载页面
路由模式vue-router
history 模式 (html5 模式): 推荐createWebHistory()
hash 模式: 不利于 SEOcreateWebHashHistory()
memory 模式: 适用于 node 环境和 SSR, url 不会改变createMemoryHistory()

location.hash 是 url 中的 hash 值, 例 https://hangtiancheng.github.io/homepage/frontend/vue-router#hash-mode, location.hash = '#hash-mode', 改变 url 中的 hash 值时, 页面不会重新加载, 通常用于单页面内的导航, 不需要服务器配置, 不利于 SEO

hash 模式和 hashchange 事件

  • Vue 路由的 hash 模式通过改变 location.hash 的值, 会触发 hashchange 事件
  • vue-router 监听 hashchange 事件, 实现无刷新的路由导航
addEventListener("hashchange", (ev) => console.log(ev));

html5 模式 (history 模式): url 中没有 #, 需要服务器配置 fallback 路由

popstate 事件

  • 改变 url 中的 hash 值时, 页面一定不会重新加载
  • 点击浏览器的前进/后退按钮改变 url 时, 会触发 popstate 事件
  • 调用 history.forward(), history.back(), history.go(delta: number) 改变 url 时, 也会触发 popstate 事件
  • 调用 history.pushState(), history.replaceState() 改变 url 时, 不会触发 popstate 事件, 页面一定不会重新加载

路由组件可以有一个唯一的名字

import {
createRouter,
createWebHistory,
type RouteRecordRaw,
} from "vue-router";
import LoginView from "@/views/LoginView.vue";
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "login",
component: () => import("@/views/LoginView.vue"),
},
{
path: "/register",
name: "register",
component: () => import("@/views/RegisterView.vue"),
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
export default router;
  • router.push 向 history 栈顶添加一条记录
  • router.replace 替换 history 栈顶的记录
const router = useRouter(); // 获取路由器对象
const routeJumpByUrl = (url: string) => {
// window.history.pushState();
router.push(url);
// router.push({ path: url, replace: false });
};
const routeJumpByName = (name: string) => {
// window.history.replaceState();
router.replace({ name, replace: true });
};
const routeJump2prev = (delta?: number) => {
// window.history.go(delta ?? -1);
router.go(delta ?? -1);
// window.history.back();
router.back();
};
const routeJump2next = (delta?: number) => {
// window.history.go(delta ?? 1);
router.go(delta ?? 1);
// window.history.forward();
router.forward();
};
  1. query: url 查询参数
  2. params: url 路径参数
  3. window.history.state
  4. 路由前置守卫
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "login",
component: () => import("@/views/LoginView.vue"),
},
{
path: "/register",
name: "register",
component: () => import("@/views/RegisterView.vue"),
},
{
path: "/register/:id/:name?/:age?", // url 路径参数
// :id 必传参数
// :name? :age? 可选参数
name: "registerWithId",
component: () => import("@/views/RegisterView.vue"),
},
];

props 设置为 true 时, route.params url 路径参数将被设置为路由组件的 props

const routes: Array<RouteRecordRaw> = [
{
path: "/register/:id/:name?/:age?", // url 路径参数
name: "registerWithId",
component: () => import("@/views/RegisterView.vue"),
props: true,
},
];

props 是一个对象时, 将该对象设置为路由组件的 props

const routes: Array<RouteRecordRaw> = [
{
path: "/register",
name: "register",
component: () => import("@/views/RegisterView.vue"),
props: { foo: "bar" },
},
];

props 是一个函数时, 将该函数的返回值设置为路由组件的 props

const routes: Array<RouteRecordRaw> = [
{
path: "/register",
name: "register",
component: () => import("@/views/RegisterView.vue"),
props: (route: RouteLocationNormalizedGeneric) => ({ ...route.query }),
},
];
<template>
<!-- <RouterView /> 等价于 -->
<RouterView v-slot="{ route, Component }">
<!-- Component 必须有唯一的根元素 -->
<component :is="Component" />
</RouterView>
</template>

使用 <Transition /> 过渡组件和 <KeepAlive /> 缓存组件

<template>
<RouterView v-slot="{ route, Component }">
<Transition>
<KeepAlive>
<component :is="Component" />
</KeepAlive>
</Transition>
</RouterView>
</template>
const routes: Array<RouteRecordRaw> = [
{
path: "/",
redirect: "/home", // 路由重定向
},
{
path: "/home",
component: () => import("@/views/HomeView.vue"),
children: [
{
path: "",
name: "login",
component: () => import("@/views/LoginView.vue"),
},
{
path: "register",
// path: "register", 实际路由 "/home/register"
// path: "/register", 实际路由 "/register"
name: "register",
component: () => import("@/views/RegisterView.vue"),
},
],
},
];

具名 <RouterView />, 路由别名, 路由重定向

Section titled “具名 <RouterView />, 路由别名, 路由重定向”
const routes: Array<RouteRecordRaw> = [
{
path: "/views",
// 路由别名
// alias: '/',
alias: ["/", "/home"],
// 路由重定向
// redirect: '/views/ab',
// redirect: {
// path: '/views/ab',
// // name: 'ab',
// },
redirect: (to) => {
console.log("[redirect] to:", to);
return {
// path: '/views/ab',
name: "ab",
query: to.query, // 默认
};
},
children: [
{
path: "/views/ab", // path: 'ab'
name: "ab",
components: {
// name="default"
default: () => import("@/views/AView.vue"),
// name="pageB"
pageB: () => import("@/views/BView.vue"),
},
},
{
path: "bc", // path: '/views/bc'
name: "bc",
components: {
// name="pageB"
pageB: () => import("@/views/BView.vue"),
// name="pageC"
pageC: () => import("@/views/CView.vue"),
},
},
],
},
];
  • 前置守卫函数在 redirect 重定向后, 路由跳转前执行
  • 后置守卫函数在路由跳转后执行

router.beforeEach((to, from, next) => void)

const whitelist: string[] = ["/register", "/login"];
router.beforeEach(
(
to, // (重定向后的) 目的路由
from, // 源路由
next, // 放行函数
) => {
console.log("[beforeGuard] from:", from);
console.log("[beforeGuard] to:", to);
if (whitelist.includes(to.path) || sessionStorage.getItem("token")) {
next(); // 放行
} else {
next("/login"); // 重定向到登录
}
},
);

router.afterEach((to, from) => void)

Demo: Progress Bar

<script lang="ts" setup>
import { computed, ref } from "vue";
const progress = ref(0);
const barWidth = computed(() => progress.value + "%");
let requestId = 0;
const loadStart = () => {
progress.value = 0;
const cb = () => {
if (progress.value < 100) {
progress.value++;
requestId = requestAnimationFrame(cb);
} else {
progress.value = 0;
cancelAnimationFrame(requestId);
}
};
// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
// 要求浏览器在下一次重绘前, 调用回调函数 cb
requestId = requestAnimationFrame(cb);
};
const loadEnd = () => {
progress.value = 100;
setTimeout(() => {
requestId = requestAnimationFrame(() => {
progress.value = 0;
});
}, 300);
};
defineExpose({ loadStart, loadEnd });
</script>
<template>
<div class="fixed top-0 h-[3px] w-dvw">
<div class="bar h-[inherit] w-0 bg-lime-100" />
</div>
</template>
<style lang="css" scoped>
.bar {
width: v-bind(barWidth);
}
</style>
import {
createRouter,
createWebHistory,
type RouteRecordRaw,
} from "vue-router";
declare module "vue-router" {
interface RouteMeta {
title: string;
transition?: string;
}
}
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: () => import("@/views/HomeView.vue"),
// 路由元信息
meta: {
title: "Homepage",
// 路由过渡动画
transition: "animate__bounceIn",
},
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
router.beforeEach((to /** from, next */) => {
if (to.meta.title) {
document.title = to.meta.title;
}
});
export default router;

仅点击浏览器的前进/后退按钮 (触发 popstate 事件) 时可用

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
// 滚动行为
scrollBehavior: (to, from, savedPosition) => {
// 滚动到原位置
if (savedPosition) {
return savedPosition;
}
// 滚动到锚点
if (to.hash) {
return { el: to.hash, behavior: "smooth" };
}
// 滚动到顶部
return { top: 0 };
},
});
  • router.addRoute() 动态添加路由, 返回删除该路由的函数
  • router.removeRoute() 动态删除路由
  • router.hasRoute() 判断路由是否存在
  • router.getRoutes() 获取所有路由信息

示例: 根据后端的响应, 动态添加路由

import { fileURLToPath, URL } from "node:url";
import { defineConfig, type Plugin } from "vite";
import vue from "@vitejs/plugin-vue";
import url from "node:url";
const vitePluginServer = (): Plugin => {
return {
name: "vite-plugin-server",
configureServer(server) {
server.middlewares.use("/routes", (req, res) => {
res.setHeader("Content-Type", "application/json");
const queryParams = url.parse(
req.originalUrl!,
true /** parseQueryString */,
).query;
const { username } = queryParams;
let resData: {
routes: { path: string; name: string; component: string }[];
} = {
routes: [],
};
switch (username) {
case "admin":
resData = {
routes: [
{ path: "/admin", name: "admin", component: "AdminView" },
{ path: "/admin2", name: "admin2", component: "AdminView2" },
],
};
break;
default:
resData = {
routes: [
{ path: "/user", name: "user", component: "UserView" },
{ path: "/user2", name: "user2", component: "UserView2" },
],
};
break;
}
res.end(JSON.stringify(resData));
});
},
};
};
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vitePluginServer()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});