Vue-Router 官网速通(上)https://developer.aliyun.com/article/1513239?spm=a2c6h.13148508.setting.27.f8774f0euyBLtl
三. 进阶
1. 导航守卫
导航守卫通过跳转或取消的方式守卫导航。路由导航包括:全局,单个路由,组件级。
1.1 全局前置守卫
使用 router.beforeEach 注册全局前置守卫,导航跳转前触发,接受两个参数,to 即将进入的路由,from 即将离开的路由,返回 false 是取消当前跳转,返回一个路由是跳转到返回路由中:
const router = createRouter({ ... }) router.beforeEach(async (to, from) => { if ( // 检查用户是否已登录 !isAuthenticated && // ❗️ 避免无限重定向 to.name !== "Login" ) { // 将用户重定向到登录页面 return { name: "Login" }; } });
1.2 全局解析守卫
使用 router.beforeResolve 注册全局解析守卫。解析守卫在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。是获取数据或执行任何其他操作(如用户无法进入页面时希望避免执行的操作)的理想位置。例如:确保用户可以访问自定义 meta 属性 requiresCamera 的路由:
router.beforeResolve(async (to) => { if (to.meta.requiresCamera) { try { await askForCameraPermission(); } catch (error) { if (error instanceof NotAllowedError) { // ... 处理错误,然后取消导航 return false; } else { // 意料之外的错误,取消导航并把错误传给全局处理器 throw error; } } } });
1.3 全局后置钩子
使用 router.afterEach 注册全局后置钩子,后置钩子不会改变导航本身,可以用于分析、更改页面标题、声明页面等辅助功能以及许多其他事情:
router.afterEach((to, from) => { sendToAnalytics(to.fullPath); });
1.4 在守卫内的全局注入
使用 inject() 方法在导航守卫内注入全局属性。app.provide() 中所有内容都可以在 router.beforeEach()、router.beforeResolve()、router.afterEach() 内获取到:
// main.ts const app = createApp(App); app.provide("global", "hello injections"); // router.ts or main.ts router.beforeEach((to, from) => { const global = inject("global"); // 'hello injections' // a pinia store const userStore = useAuthStore(); // ... });
1.5 路由独享的守卫
在路由配置上定义 beforeEnter 守卫,在进入路由时触发,不会在 params、query、hash 改变时触发:
const routes = [ { path: "/users/:id", component: UserDetails, beforeEnter: (to, from) => { // reject the navigation return false; }, }, ];
也可以将函数数组传给 beforeEnter,这在为不同的路由重用守卫时很有用:
function removeQueryParams(to) { if (Object.keys(to.query).length) return { path: to.path, query: {}, hash: to.hash }; } function removeHash(to) { if (to.hash) return { path: to.path, query: to.query, hash: "" }; } const routes = [ { path: "/users/:id", component: UserDetails, beforeEnter: [removeQueryParams, removeHash], }, { path: "/about", component: UserDetails, beforeEnter: [removeQueryParams], }, ];
1.6 组件内的守卫
在路由组件内直接定义路由导航守卫,onBeforeRouteLeave 离开组件时调用,onBeforeRouteUpdate 路由更新时调用:
import { onBeforeRouteLeave, onBeforeRouteUpdate } from "vue-router"; onBeforeRouteLeave((to, from) => {}); onBeforeRouteUpdate((to, from) => {});
1.7 完整的导航解析流程
导航被触发。 在失活的组件里调用 onBeforeRouteLeave 守卫。 调用全局的 beforeEach 守卫。 在重用的组件里调用 onBeforeRouteUpdate 守卫(2.2+)。 在路由配置里调用 beforeEnter。 解析异步路由组件。 调用全局的 beforeResolve 守卫(2.5+)。 导航被确认。 调用全局的 afterEach 钩子。 触发 DOM 更新。 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入
2. 路由元信息
使用 meta 定义路由元信息,可以存放任何信息,如过渡名,路由访问权限等,可以在导航守卫被访问到:
const routes = [ { path: '/posts', component: PostsLayout, children: [ { path: 'new', component: PostsNew, // 只有经过身份验证的用户才能创建帖子 meta: { requiresAuth: true }, }, { path: ':id', component: PostsDetail // 任何人都可以阅读文章 meta: { requiresAuth: false }, } ] } ]
怎么访问 meta 字段呢?
router.beforeEach((to, from) => { // 而不是去检查每条路由记录 // to.matched.some(record => record.meta.requiresAuth) if (to.meta.requiresAuth && !auth.isLoggedIn()) { // 此路由需要授权,请检查是否已登录 // 如果没有,则重定向到登录页面 return { path: "/login", // 保存我们所在的位置,以便以后再来 query: { redirect: to.fullPath }, }; } });
2.1 TypeScript
可以继承来自 vue-router 中的 RouteMeta 来为 meta 字段添加类型:
// 添加到一个 `.d.ts` 文件中。将这个文件包含在项目的 `tsconfig.json` 中的 "file" 字段内。 import "vue-router"; // 为了确保这个文件被当作一个模块,添加至少一个 `export` 声明 export {}; declare module "vue-router" { interface RouteMeta { // 是可选的 isAdmin?: boolean; // 每个路由都必须声明 requiresAuth: boolean; } }
3. 数据获取
获取数据可以导航后在组件生命周期获取,也可以在导航前,在路由守卫获取。
3.1 导航完成后获取数据
马上导航和渲染组件,在 created 钩子中获取数据。
<script setup lang="ts"> import { useRoute } from "vue-router"; import { watchEffect, reactive } from "vue"; const route = useRoute(); watchEffect(() => { fetchData(route.params.id); }); const post = reactive(null); const fetchData = (id) => { getPost(id, (err, post) => { post = post; }); }; </script> <template> <div class="post"> <div class="content"> <h2>{{ post.title }}</h2> <p>{{ post.body }}</p> </div> </div> </template>
4. 组合式 API
4.1 在 setup 中访问路由和当前路由
不能直接访问 this.router或this.router 或 this.route。需使用 useRouter 和 useRoute 函数,但在模板中可以访问 router和router 和 route:
<script setup lang="ts"> import { useRoute, useRouter } from "vue-router"; const route = useRoute(); const router = useRouter(); const handleGo = () => { router.push({ name: "Home", query: { ...route.query } }); }; </script> <template> <div class="demo" @click="handleGo">{{ $route.params }}</div> </template>
4.2 导航守卫
onBeforeRouteLeave,onBeforeRouteUpdate 离开和更新守卫:
<script setup lang="ts"> import { useRouter, onBeforeRouteLeave, onBeforeRouteUpdate, } from "vue-router"; const router = useRouter(); onBeforeRouteUpdate(() => { console.log("更新路由"); }); onBeforeRouteLeave(() => { console.log("离开路由"); }); const changeRoute = () => { router.push("/demo/2"); }; </script> <template> <div class="demo" @click="changeRoute">改变路由</div> </template>
4.3 useLink
利用 RouterLink 构建自己的 RouterLink 组件或生成自定义链接:
<script setup lang="ts"> import { RouterLink } from "vue-router"; import { computed } from "vue"; const props = defineProps({ // @ts-ignore ...RouterLink.props, inactiveClass: String, _target: { type: String, default: "_self", }, }); const isExternalLink = computed( () => typeof props.to === "string" && props.to.startsWith("http") ); </script> <template> <a v-if="isExternalLink" :href="to" :target="_target"><slot /></a> <router-link v-else v-bind="$props" :to="$props.to"><slot /></router-link> </template>
5. RouterView 插槽
RotuerView 暴露插槽,用来渲染路由组件:
<router-view v-slot="{ Component }"> <component :is="Component" /> </router-view> <!-- 等价于 --> <router-view />
5.1 KeepAlive & Transition
KeepAlive 是保持路由组件活跃,而不是 RouterView 本身。所以需要将 KeepAlive 放置在插槽内:
<router-view v-slot="{ Component }"> <keep-alive> <component :is="Component" /> </keep-alive> </router-view>
同理 Transition 实现路由组件之间切换的过渡效果:
<router-view v-slot="{ Component }"> <transition> <component :is="Component" /> </transition> </router-view>
5.2 模板引用
可以将模板引用放置在路由组件上:
<router-view v-slot="{ Component }"> <component :is="Component" ref="mainContent" /> </router-view>
6 过渡动效
要实现路由组件切换需要使用 插槽:
<router-view v-slot="{ Component }"> <transition name="fade"> <component :is="Component" /> </transition> </router-view>
6.1 单个路由的过渡
使用元信息和动态 name 结合实现每个路由的不同过渡效果:
const routes = [ { path: "/custom-transition", component: PanelLeft, meta: { transition: "slide-left" }, }, { path: "/other-transition", component: PanelRight, meta: { transition: "slide-right" }, }, ];
<router-view v-slot="{ Component, route }"> <!-- 默认过渡效果 `fade` --> <transition :name="route.meta.transition || 'fade'"> <component :is="Component" /> </transition> </router-view>
6.2 复用视图进行过渡
复用路由组件会忽略过渡,可以添加 key 属性来强制过渡:
<router-view v-slot="{ Component, route }"> <transition name="fade"> <component :is="Component" :key="route.path" /> </transition> </router-view>
7. 滚动行为
通过 scrollBehavior 实现滚动效果,接收 to 和 from 路由对象:
const router = createRouter({ history: createWebHashHistory(), routes: [...], scrollBehavior (to, from) { // return 期望滚动到哪个的位置 return { top: 0 } } })
通过 el 传递 CSS 选择器或 DOM 元素。top 和 left 的偏移量相对于该元素。
const router = createRouter({ scrollBehavior(to, from) { // 始终在元素 #main 上方滚动 10px return { // 也可以这么写 // el: document.getElementById('main'), el: "#main", // 在元素上 10 像素 top: 10, }; }, });
模拟 “滚动到锚点” 的行为:
const router = createRouter({ scrollBehavior(to, from, savedPosition) { if (to.hash) { return { el: to.hash, }; } }, });
如果浏览器支持滚动行为,设置 behavior 让它变得更流畅:
const router = createRouter({ scrollBehavior(to, from, savedPosition) { if (to.hash) { return { el: to.hash, behavior: "smooth", }; } }, });
7.1 延迟滚动
通过返回一个 Promise,实现延时滚动:
const router = createRouter({ scrollBehavior(to, from, savedPosition) { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ left: 0, top: 0 }); }, 500); }); }, });
8. 路由懒加载
当打包时,JS 包会非常大,影响用户体验,使用动态导入代替静态导入:
// 将 // import UserDetails from './views/UserDetails.vue' // 替换成 const UserDetails = () => import('./views/UserDetails.vue') const router = createRouter({ // ... routes: [ { path: '/users/:id', component: UserDetails } // 或在路由定义里直接使用它 { path: '/users/:id', component: () => import('./views/UserDetails.vue') }, ], })
8.1 把组件按组分块
8.1.1 使用 webpack
把某个路由下的所有组件打包到同个异步块 (chunk) 中。使用命名 chunk,特殊的注释语法:
const UserDetails = () => import(/* webpackChunkName: "group-user" */ "./UserDetails.vue"); const UserDashboard = () => import(/* webpackChunkName: "group-user" */ "./UserDashboard.vue"); const UserProfileEdit = () => import(/* webpackChunkName: "group-user" */ "./UserProfileEdit.vue");
8.1.2 使用 Vite
使用 rollupOptions 下定义分块:
// vite.config.js export default defineConfig({ build: { rollupOptions: { // https://rollupjs.org/guide/en/#outputmanualchunks output: { manualChunks: { "group-user": [ "./src/UserDetails", "./src/UserDashboard", "./src/UserProfileEdit", ], }, }, }, }, });
9. 扩展 RouterLink
自定义 RouterLink 实现导航菜单链接,处理外部链接:
<script setup lang="ts"> import { RouterLink } from "vue-router"; import { computed } from "vue"; const props = defineProps({ // @ts-ignore ...RouterLink.props, _target: { type: String, default: "_self", }, }); const isExternalLink = computed( () => typeof props.to === "string" && props.to.startsWith("http") ); </script> <template> <a v-if="isExternalLink" v-bind="$attrs" :href="to" :target="_target" ><slot /></a> <router-link v-else v-bind="$props" :to="$props.to"><slot /></router-link> </template>
10. 导航故障
10.1 检测导航故障
如果导航被阻,可以通过 router.push 返回的 Promise 的解判断是否离开了当前位置:
const navigationResult = await router.push("/my-profile"); if (navigationResult) { // 导航被阻止 } else { // 导航成功 (包括重新导航的情况) this.isMenuOpen = false; }
10.2 全局导航故障
使用 router.afterEach() 检测全局导航故障:
router.afterEach((to, from, failure) => { if (failure) { sendToAnalytics(to, from, failure); } });
11. 动态路由
11.1 添加/移除 路由
使用 router.addRoute() 和 router.removeRoute() 实现动态路由的添加和删除。
router.addRoute({ path: "/about", component: About }); router.removeRoute("About"); // 使用 name 移除
11.2 在导航守卫中添加路由
通过返回新的位置来触发重定向:
router.beforeEach((to) => { if (!hasNecessaryRoute(to)) { router.addRoute(generateRoute(to)); // 触发重定向 return to.fullPath; } });
11.3 添加嵌套路由
将路由的 name 作为第一个参数传递给 router.addRoute():
router.addRoute({ name: "admin", path: "/admin", component: Admin }); router.addRoute("admin", { path: "settings", component: AdminSettings }); // 等价于 router.addRoute({ name: "admin", path: "/admin", component: Admin, children: [{ path: "settings", component: AdminSettings }], });
11.4 查看现有路由
Vue Router 提供了两个功能来查看现有的路由:
| router.hasRoute(routerName):检查路由是否存在。
| router.getRoutes():获取一个包含所有路由记录的数组。