Vue Router 官网已经写得很好了,这里自己再总结巩固下。(注意:这里所有的例子都是基于Vue/cli 3.4的脚手架进行讲解的)
Vue的路由分两种:编程式路由和声明式路由
vm.$route.push('/路由地址') //编程式路由
<router-link to="/路由地址"></router-link> //声明式路由
Vue Router可以分以下几个模块来讲解
- 动态路由匹配
- 嵌套路由
- 编程式导航
- 命名路由
- 命名视图
- 重定向和别名
- 路由组件传参
- H5 History模式
- 导航守卫
- 路由元信息
- 过渡动效(本文不做介绍)
- 数据获取
- 滚动行为
- 路由懒加载
动态路由匹配
【应用场景】
假设我们有个模板是展示用户信息的,在上一个模板中,点击不同的用户ID进入相同的模板,但是展示的用户信息是不一样的。就像这样:
/user/user1
和/user/user2
,当然还有其他方式,我们先用这种方式。看代码:
<!-- 先写一个视图用于放置声明式路由 -->
<template>
<div>
home
<router-link to="/componentA/user1">user1</router-link>
<router-link to="/componentA/user2">user2</router-link>
</div>
</template>
<script>
// @ is an alias to /src
export default {
}
</script>
//在脚手架里的router.js里配置路由
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import componentA from './components/componentA.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/componentA/:id/', //这里就是动态路由,通过id来进行动态匹配
component: componentA
}
]
})
<!-- componentA -->
<template>
<div>
componentA: {{$route.params.id}}
<button @click="goBack">goBack</button>
</div>
</template>
<script>
export default {
name: 'componentA',
methods: {
goBack() {
this.$router.push('/')
},
}
}
</script>
<style lang="">
</style>
【响应路由参数变化注意点】
- 组件会被复用
- 组件生命周期钩子不会被调用
- 可以通过 watch方法或者 beforeRouteUpdate监听动态路由变化
watch() {
'$route'(to, from) {
//...
}
},
beforeRouteUpdate(to, from, next) {
console.log("to", to);
console.log("from", from);
console.log("next", next);
},
嵌套路由
【应用场景】
比如做个tab页,就像下图这样:
这个时候就要用到嵌套视图了,看代码:
现在App.vue里放个路由视图(也就是<router-view></router-view
这个组件)
<template>
<!-- App.vue -->
<div id="app">
<router-view></router-view>
</div>
</template>
<style lang="less">
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
先写一个组件充当父路由的组件
<template>
<!-- tab.vue -->
<div class="tab">
<router-link to="/index/a">首页</router-link>
<router-link to="/index/b">技术页</router-link>
<!-- 这里很重要,如果父路由里的组件里没有相应的路由视图,那么子路由导航后是无法展现的 -->
<router-view></router-view>
</div>
</template>
<script>
import Vue from 'vue';
// @ is an alias to /src
export default {
name: 'tab',
};
</script>
<style scoped>
.tab {
background-color: yellow;
}
</style>
然后随便写2个组件视图
<template>
<!-- componentA_a.vue -->
<div class="index">
<div class="index">
首页
</div>
</div>
</template>
<script>
export default {
name: "componentA_a",
};
</script>
<style scoped>
.index {
background-color: red;
}
</style>
<template>
<!-- componentA_b.vue -->
<div class="tec">
<div class="tec">技术页</div>
</div>
</template>
<script>
export default {
name: "componentA_b",
created() {
console.log(123);
}
};
</script>
<style scoped>
.tec {
background-color: lightblue;
}
</style>
最后配置router.js
import Vue from 'vue'
import Router from 'vue-router'
import tab from './views/tab.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
redirect: '/index' //重定向到/index路由
},
{
path: '/index',
component: tab,
children: [
{
path: '',
redirect: 'a' //重定向路由,当路径是空的时候可以重定向路由也可以提供一个组件
},
{
path: 'a',
name: "componentA_a",
component: () => import('./components/componentA_a.vue')
},
{
path: 'b',
name: "componentA_b",
component: () => import('./components/componentA_b.vue')
}
]
}
]
})
这样就实现了嵌套路由,效果就是前面那张图。
PS:
- 以 / 开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。
- 如果父路由里的组件里没有相应的路由视图,那么子路由导航后是无法展现的
编程式导航
【应用场景】
大多数情况还是编程式导航的吧,毕竟编程式导航用起来更加灵活。我可以对任务组件绑定事件,触发路由导航,而且可以随性所欲的传参。
3个方法
-
router.push(location, onComplete?, onAbort?)
想要导航到不同的 URL,则使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
PS: <router-link :to="..."> 等同于调用 router.push(...)。
该方法的参数可以是一个字符串路径,或者一个描述地址的对象。例如:
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
注意:如果提供了 path,params 会被忽略,上述例子中的 query 并不属于这种情况。取而代之的是下面例子的做法,你需要提供路由的 name 或手写完整的带有参数的 path:
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
同样的规则也适用于 router-link 组件的 to 属性。
在 2.2.0+,可选的在 router.push 或 router.replace 中提供 onComplete 和 onAbort 回调作为第二个和第三个参数。这些回调将会在导航成功完成 (在所有的异步钩子被解析之后) 或终止 (导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由) 的时候进行相应的调用。
注意: 如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1
-> /users/2
),你需要使用 beforeRouteUpdate
来响应这个变化 (比如抓取用户信息)。
-
router.replace(location, onComplete?, onAbort?)
PS:<router-link :to="..." replace> 等同于router.replace(...)
跟 router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。 -
router.go(n)
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)
// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)
// 后退一步记录,等同于 history.back()
router.go(-1)
// 前进 3 步记录
router.go(3)
// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)
router.go(100)
命名路由
在前面的例子中你可能已经注意到了,比如这样
router.push({ name: 'user', params: { userId: '123' }})
或者
<router-link :to="{ name: 'user', params: { userId: 123 }}"></router-link>
这2种方法是等效的,这样就省去了path参数,
命名视图
【应用场景】
比如你一个路由里的组件需要多个子组件布局,这几个子组件是公共复用的,那么通过命名路由视图的方式将大大降低你组件的耦合度和发杂性。
看代码:
<template>
<!-- App.vue -->
<div id="app">
<router-view name="header"></router-view>
<router-view name="body"></router-view>
</div>
</template>
<style lang="less">
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
//在脚手架里的router.js里配置路由
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import componentA from './components/componentA.vue'
import componentB from './components/componentB.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
components: { //加个s
body: componentB,
header: componentA,
}
}
]
})
把刚才视图组件补齐
<template>
<!-- componentA.vue -->
<div>
<div class="header">
header
</div>
</div>
</template>
<script>
export default {
name: 'componentA',
}
</script>
<style lang="less" scoped>
.header {
background-color: lightgreen;
}
</style>
<template>
<!-- componentB.vue -->
<div>
<div class="bodyer">
body
</div>
</div>
</template>
<script>
export default {
name: 'componentB',
}
</script>
<style scoped lang="less">
.bodyer {
background-color: lightgrey;
}
</style>
效果:
重定向和别名
【应用场景】
重定向的应用场景还是比较多的吧,经常用的就有默认路由重定向,别名就是给配置的路由名换个马甲,隐藏下真实的路由名吧,其他用处还没想到。
看重定向例子:
重定向也是通过 routes 配置来完成,下面例子是从 /a 重定向到 /b:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: '/b' }
]
})
重定向的目标也可以是一个命名的路由:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
})
甚至是一个方法,动态返回重定向目标:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
]
})
看别名例子:
/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。
绝对别名
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
默认别名
const router = new VueRouter({
routes: [
{ path: 'default', component: Default, alias: '' },
]
})
多个别名
const router = new VueRouter({
routes: [
{ path: 'baz', component: Baz, alias: ['/baz', 'baz-alias'] },
]
})
路由组件传参
先看代码
<template>
<div>
<router-link to="/b/c">c</router-link>// 这里我传了个参数“c”
</div>
</template>
<script>
// @ is an alias to /src
export default {
}
</script>
//在脚手架里的router.js里配置路由
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import componentA from './components/componentA.vue'
import componentB from './components/componentB.vue'
Vue.use(Router)
const router = new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/b/:id',
name: 'b',
component: componentB,
props: (route) => ({ //这里也可以通过props: true, 或者 props: {id : true}来传参
id: route.params.id
})
}
]
});
export default router;
<template>
<!-- componentB.vue -->
<div>
<div class="bodyer">
body: {{id}}
</div>
</div>
</template>
<script>
export default {
props: ['id'], //通过props接受参数
name: 'componentB',
}
</script>
<style scoped lang="less">
.bodyer {
background-color: lightgrey;
}
</style>
HTML5 History 模式
Vue-router是基于H5的history模式来实现的
导航守卫
记住参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route
对象来应对这些变化,或使用 beforeRouteUpdate
的组件内守卫。
全局前置守卫
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
to: 即将要进入的目标 路由对象
from: 当前导航正要离开的路由
next`:
next(false),表示终止跳转
next('需要跳转的目标路由')
next(可以传入router.push支持的任意内容')
全局解析守卫
router.beforeResolve和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
全局后置钩子
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
router.afterEach((to, from) => {
// ...
})
路由独享的守卫(beforeEnter)
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件内的守卫
- beforeRouteEnter
- beforeRouteUpdate (2.2 新增)
- beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
next(vm => {
console.log(vm)//vm就是组件实例,也就是this
})
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
完整的解析流程
完整的导航解析流程
导航被触发。
在失活的组件里调用离开守卫。beforeRouteleave
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
路由元信息
这个东西特别适合权限检查,比如说登录。在路由配置里给每个路由做个标记,需要检查权限的标记下,那么不同权限就可以按照权限标准浏览页面了,岂不是美滋滋。
看代码:
<template>
<!-- Home.vue -->
<div>
欢迎进入首页
</div>
</template>
<script>
// @ is an alias to /src
export default {
}
</script>
//在脚手架里的router.js里配置路由
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import login from './views/login.vue'
import componentA from './components/componentA.vue'
import componentB from './components/componentB.vue'
Vue.use(Router)
const router = new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home,
meta: {
isRequireLogin: true
},
beforeEnter(to, from, next) { //具体判断逻辑不写了
let judeLogin = () => {
let a = Math.random();
console.log(a)
if(a > 0.5) {
return false
}else {
return true
}
}
console.log(to.matched)
if(to.matched.some(record => record.meta.isRequireLogin)) {
if(judeLogin()) {
next()
} else {
next('/login')
}
}else {
next('/login')
}
}
},
{
path: '/login',
name: 'login',
component: login
}
]
});
export default router;
<template>
<!-- login.vue -->
<div>
登录页面
</div>
</template>
<script>
// @ is an alias to /src
export default {
methods: {
}
}
</script>
看效果:这里姑且当随机数大于0.5的时候,显示登录失败
数据获取
有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:
导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示。
导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。
个人觉得如果后端数据快的话用后面一种,快的话用前面那种。
第一种不用说吧,直接在生命钩子create()里来实现,后面那种看代码:
beforeRouteEnter (to, from, next) {
//getPost是自定义的请求接口
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
只要在beforeRouteEnter 这个路由守卫里实现就好了
路由懒加载
当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
庆幸的是Vue/cli脚手架默认就是懒加载(webpack的方式),其他方式我暂时用不到,有兴趣的话可以看官网。
【例子】
import('./Foo.vue') // 返回 Promise
【完】