主题
Vue Router 3 使用笔记
汇总 Vue 2 项目中 vue-router 3.x 的安装、路由配置、导航与传参方式,以及导航守卫、路由元信息、过渡动效、数据获取、滚动行为与懒加载等进阶用法。
一、安装
bash
cnpm install --save vue-router二、基本使用
路由相关代码拆成三部分:routes.js 维护路由表、index.js 创建 router 实例、main.js 把实例注入根实例。
1. routes.js
定义路由表:
js
const hello = () => import("../components/HelloWorld.vue");
const routes = [
{
path: "/",
redirect: "/a",
},
{
path: "/a",
component: hello, // 注意是 component,不是 components
},
];
export default routes;2. index.js
创建 VueRouter 实例并导出:
js
import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes";
Vue.use(VueRouter);
const router = new VueRouter({
routes,
});
export default router;3. main.js
把 router 注入根实例,让整个应用都具备路由功能:
js
import router from "./router/index";
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");注意:路由内容不显示,且控制台报
Cannot read property '$createElement' of undefined,通常是因为routes.js中把component错拼成了components。带 s 的components是「命名视图」的写法,普通路由只接受component。
三、声明式与编程式导航
- 声明式导航:在模板中通过
<router-link :to="...">完成跳转。 - 编程式导航:在脚本中通过
this.$router.push(...)完成跳转。
html
<router-link to="/register">注册</router-link>js
this.$router.push("/register");四、嵌套路由
App.vue 中的 <router-view> 是顶层出口,渲染最高级路由匹配到的组件。如果希望 home 页左侧导航栏保持不变、右侧内容随路由切换,就可以在 home 路由下再嵌套一层子路由:
js
{
path: '/',
component: Home,
name: 'Home',
children: [
{ path: '/fleetStatus', meta: 'fleetStatus', component: FleetStatus },
],
}五、命名路由
使用场景:当子路由路径过长时,用一个名称来标识路由会更方便。
1. 定义路由
js
{
name: 'register',
component: register,
}2. 传参
js
this.$router.push({
name: "register",
params: {
plan: "private",
},
});3. 获取参数
js
this.$route.params.plan;此时 URL 仍然是 /register。
注意:
params不会拼接在 URL 上,刷新页面会丢失数据。
六、命名视图
如果想同级展示多个视图,而不是嵌套展示,就可以使用命名视图。典型场景:首页需要一个绿色的 header,其他页面的头部则用红色的 header。
如果 <router-view> 没有设置名字,那么默认为 default。
1. 完整示例
下面示例通过 CDN 引入 Vue 与 Vue Router,无需构建工具:
html
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 导入 vue 路由的包 -->
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>页面结构:
html
<div id="app">
<!-- 默认容器 -->
<router-view></router-view>
<!-- 使用 css3 的 flex 布局,包起来 -->
<div class="container">
<!-- 给容器指定 name 名称,加载对应组件 -->
<router-view name="left"></router-view>
<router-view name="main"></router-view>
</div>
</div>
<template id="headerBox">
<h1 class="header">headerBox 组件</h1>
</template>
<template id="leftBox">
<h1 class="left">leftBox 组件</h1>
</template>
<template id="mainBox">
<h1 class="main">mainBox 组件</h1>
</template>样式:
css
* {
margin: 0;
padding: 0;
}
.header {
height: 80px;
background-color: orange;
}
.container {
display: flex;
height: 600px;
}
.left {
background-color: lightgreen;
flex: 2;
}
.main {
background-color: lightpink;
flex: 8;
}脚本:
js
var headerBox = {
template: "#headerBox",
};
var leftBox = {
template: "#leftBox",
};
var mainBox = {
template: "#mainBox",
};
var router = new VueRouter({
routes: [
{
path: "/",
// components(复数)同时展示多个组件
components: {
default: headerBox, // 默认容器放 headerBox 组件
left: leftBox, // name 为 left 的容器放 leftBox 组件
main: mainBox, // name 为 main 的容器放 mainBox 组件
},
},
],
});
var vm = new Vue({
el: "#app",
data: {},
router,
});2. 嵌套命名视图
嵌套命名视图可以用来实现复杂布局。
text
/settings/emails /settings/profile
+-----------------------------------+ +------------------------------+
| UserSettings | | UserSettings |
| +-----+-------------------------+ | | +-----+--------------------+ |
| | Nav | UserEmailsSubscriptions | | | | Nav | UserProfile | |
| | +-------------------------+ | | | +--------------------+ |
| | | | | | | | UserProfilePreview | |
| +-----+-------------------------+ | | +-----+--------------------+ |
+-----------------------------------+ +------------------------------+Nav只是一个常规组件。UserSettings是一个视图组件。UserEmailsSubscriptions、UserProfile、UserProfilePreview是嵌套的视图组件。
html
<!-- UserSettings.vue -->
<div>
<h1>User Settings</h1>
<NavBar />
<router-view />
<router-view name="helper" />
</div>js
{
path: '/settings',
// 也可以在顶级路由就配置命名视图
component: UserSettings,
children: [
{
path: 'emails',
component: UserEmailsSubscriptions,
},
{
path: 'profile',
components: {
default: UserProfile,
helper: UserProfilePreview,
},
},
],
}七、重定向与别名
1. 重定向
重定向是路由替换:当用户访问 /a 时,URL 会被替换成 /b,然后匹配到路由 /b。
从 /a 重定向到 /b:
js
const router = new VueRouter({
routes: [{ path: "/a", redirect: "/b" }],
});重定向的目标也可以是一个命名路由:
js
const router = new VueRouter({
routes: [{ path: "/a", redirect: { name: "foo" } }],
});重定向的目标还可以是一个方法:
js
const router = new VueRouter({
routes: [
{
path: "/a",
redirect: (to) => {
// 方法接收目标路由作为参数
// return 重定向的字符串 / 路径对象
},
},
],
});2. 别名
别名是路由映射:/a 的别名是 /b,意味着用户访问 /b 时,URL 保持为 /b,但路由匹配的是 /a,就像用户访问 /a 一样。
js
const router = new VueRouter({
routes: [{ path: "/a", component: A, alias: "/b" }],
});八、路由传参
1. query 传参
js
// 路由定义
{
path: '/register',
component: register,
}
// 页面传参
this.$router.push({
path: '/register',
query: {
plan: 'private',
},
})
// 页面获取
this.$route.query.plan2. 动态路由匹配
js
// 路由定义
{
path: '/register/:plan',
component: register,
}
// 页面传参
this.$router.push(`/register/private`)
// 页面获取
this.$route.params.plan拓展:在组件中使用 $route 会使组件与其对应路由形成高度耦合,组件只能在特定 URL 上使用,限制了灵活性。可以使用 props 属性进行解耦:
js
// 路由定义
{
path: '/register/:plan',
component: register,
props: true, // 设为 true 后,$route.params 会被设置为组件属性
}
// 页面传参
this.$router.push(`/register/private`)
// 页面获取
props: {
plan: {
type: String,
default: '',
},
}3. 命名路由传参
js
// 路由定义
{
name: 'register',
component: register,
}
// 页面传参
this.$router.push({
name: 'register',
params: {
plan: 'private',
},
})
// 页面获取
this.$route.params.plan注意:参数不会拼接在路由后面,刷新页面会丢失数据。
4. 几种传参方式对比
| 方式 | 路由定义 | 传参 | 获取 | URL 形式 |
|---|---|---|---|---|
| query | path: '/register' | push({ path, query }) | $route.query.plan | /register?plan=xxx |
| 动态路由 | path: '/register/:plan' | push('/register/private') | $route.params.plan | /register/private |
| 命名路由 | name: 'register' | push({ name, params }) | $route.params.plan | /register(不含参数) |
九、导航守卫
1. 全局前置守卫 router.beforeEach
使用场景:处理危险情况,例如用户跳过登录页面直接访问内页,导致即使不输入账号密码也能访问到里面的页面。
js
router.beforeEach((to, from, next) => {
if (to.path === "/login") {
Vue.$cookies.keys().forEach((item) => Vue.$cookies.remove(item));
}
const token = Vue.$cookies.get("token");
if (!token && to.path !== "/login") {
next({ path: "/login" });
} else {
next();
}
});当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行的,此时导航在所有守卫 resolve 完之前一直处于等待状态。
注意:必须调用
next方法,否则钩子不会被 resolve。
在全局前置钩子中获取 vuex 的值(此时组件未挂载,拿不到 this.$store):
js
router.app.$options.store;2. 路由独享守卫 beforeEnter
可以在路由配置上直接定义 beforeEnter 守卫:
js
const router = new VueRouter({
routes: [
{
path: "/foo",
component: Foo,
beforeEnter: (to, from, next) => {
// ...
},
},
],
});这些守卫与全局前置守卫的方法参数是一样的。
3. 组件内守卫
js
export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不能获取组件实例 this,因为守卫执行前组件实例还没被创建
// 可以传一个回调给 next 来访问组件实例,在导航被确认时执行回调,
// 并把组件实例作为回调方法的参数
next((vm) => {
vm.$data.tab = to.meta.tab ? to.meta.tab : 0;
});
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但该组件被复用时调用
// 例如带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转时,
// 由于渲染的是同一个 Foo 组件,组件实例会被复用,这个钩子就会在此情况下被调用
// 可以访问组件实例 this
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 this
// 离开守卫通常用来禁止用户在还未保存修改前突然离开,可通过 next(false) 取消导航
const answer = window.confirm(
"Do you really want to leave? you have unsaved changes!",
);
if (answer) {
next();
} else {
next(false);
}
},
};十、路由元信息
定义路由时添加 meta 字段,即可配置路由元信息。
1. $route.matched
一个数组,包含当前路由的所有嵌套路径片段的路由记录:
js
const router = new VueRouter({
routes: [
// 下面的对象就是路由记录
{
path: "/foo",
component: Foo,
children: [
// 这也是个路由记录
{ path: "bar", component: Bar },
],
},
],
});上面的路由将匹配到两个对象,一个为 foo,一个为 bar。
2. 用 meta 控制登录鉴权
使用情景:一些页面不用登录可直接访问,但评论等功能或特定页面需要登录才可查看。
解决思路:通过设置路由元信息 meta: { requiresAuth: true } 决定该路由是否需要登录,再由全局前置守卫统一控制。
js
// 定义路由
routes: [
{
path: "/download",
meta: { requiresAuth: true },
component: download,
},
];js
/**
* 路由全局前置守卫
* 鉴权功能:判断页面 meta 中 requiresAuth 字段是否需要登录(true:需登录,false:不用登录)
* 需登录页面判断登录状态,跳转
*/
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
const userAuth = Cookies.get("user");
if (!userAuth) {
// 非登录状态
next({ path: "/login" });
} else {
next();
}
} else {
next();
}
});十一、过渡动效
1. 全局过渡
html
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>css
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s ease;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}2. 单个路由的过渡
在路由的 components 之外,还可以给单个 <router-view> 包裹 <transition>,只让该视图拥有过渡效果。
3. 基于路由的动态过渡
html
<!-- 使用动态的 transition name -->
<transition :name="transitionName">
<router-view></router-view>
</transition>js
// 接着在父组件内 watch $route 决定使用哪种过渡
watch: {
'$route'(to, from) {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
},
},十二、数据获取
1. 在导航完成后获取数据
在 created 生命周期钩子中获取数据。
2. 在导航完成前获取数据
js
export default {
data() {
return {
geoData: {},
};
},
beforeRouteEnter(to, from, next) {
// 不能获取组件实例 this,因为守卫执行前组件实例还没被创建
// 在回调中获取组件实例
localData.getAirData().then((res) => {
next((vm) => {
vm.$data.geoData = res.data;
});
});
},
};在为后面的视图获取数据时,用户会停留在当前界面,因此建议在数据获取期间显示进度条或别的指示。如果数据获取失败,同样有必要展示一些全局的错误提醒。
十三、滚动行为
注意:这个功能只在支持
history.pushState的浏览器中可用。
默认情况下,切换路由时滚动条会回到顶部。savedPosition 当且仅当触发导航前进/后退按钮时才可用。
1. 模仿浏览器原生滚动
js
const router = new Router({
routes,
scrollBehavior(to, from, savedPosition) {
// return 期望滚动到的位置
if (savedPosition) {
console.log("a", savedPosition);
return savedPosition;
}
return { x: 0, y: 0 };
},
});当用户通过前进/后退切换路由时,保持滚动条位置不变,否则定位到页面顶部。
2. 模拟「滚动到锚点」
js
scrollBehavior(to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash,
}
}
}3. 利用路由元信息细颗粒度控制滚动
js
const scrollBehavior = (to, from, savedPosition) => {
if (savedPosition) {
// savedPosition 只在 popstate 导航时可用
return savedPosition;
}
const position = {};
// 新导航:返回 selector 即可滚动到锚点
if (to.hash) {
position.selector = to.hash;
}
// 如果 meta 中有 scrollToTop
if (to.matched.some((m) => m.meta.scrollToTop)) {
// 未提供 selector,或 selector 未匹配到任何元素时,会使用坐标定位
position.x = 0;
position.y = 0;
}
// 返回假值或空对象时,保持当前滚动位置
return position;
};4. 异步滚动
js
scrollBehavior(to, from, savedPosition) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 500)
})
}十四、路由懒加载
1. import 与 import() 的区别
import * from '':在编译阶段执行,同步执行。import(相对路径):异步执行,返回 Promise 对象。
js
const Components1 = () => import("../components/Components1");2. 给打包后的 JS 文件命名
通过注释语法修改打包后的 js 文件名:
js
const Login = () =>
import(/* webpackChunkName: "Login" */ "@/views/AppLogin.vue");3. 把组件按组分块
在使用命名视图时,一个路由下可能有多个组件,若想把这多个组件打包到同一个 js 文件,可以通过注释语法实现:
js
const Foo = () => import(/* webpackChunkName: "group-foo" */ "./Foo.vue");
const Bar = () => import(/* webpackChunkName: "group-foo" */ "./Bar.vue");
const Baz = () => import(/* webpackChunkName: "group-foo" */ "./Baz.vue");这样三个组件的 js 代码都会打包到名为 group-foo.js 的文件中。
十五、Router 实例方法
1. router.go(n)
参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
2. router.back()
后退一步,相当于 this.$router.go(-1)。