vue-router是vue全家桶中,用来控制路由的插件
安装
依然使用npm进行安装
npm install vue-router --save
基本使用方式
// 在项目目录建立两个文件pageA.vue和pageB.vue
// pageA.vue内容如下
<template>
<div>
<p>{{content}}</p>
</div>
</template>
<script>
export default {
data () {
return {
content: '这是页面A'
}
}
}
</script>
...
// pageB.vue内容如下
<template>
<div>
<p>{{content}}</p>
</div>
</template>
<script>
export default {
data () {
return {
content: '这是页面B'
}
}
}
</script>
...
// index.js内容调整如下
import Vue from 'vue';
import app from './index.vue';
import VueRouter from 'vue-router'; // 引入vue-router
Vue.use(VueRouter); // 引用VueRouter
import pageA from './pageA.vue';
import pageB from './pageB.vue';
const routes = [
{ path: '/pageA', component: pageA },
{ path: '/pageB', component: pageB }
];
const router = new VueRouter({
routes // (简写)相当于 routes: routes,所以这个变量名在简写下是固定的不可修改,如果用key:value的形式,key值保持这个名词,value对应的变量名可变(routes: newName)
});
let vm = new Vue({
el: "#app",
router,
render: h => h(app)
});
...
// index.vue内容调整如下
// 因为我们在index.js中设置render渲染区域是index.vue,所以在index.vue中router-view针对是当前组件
// 如果你在index.js的初始化函数中设定render: h => h('router-view'),同时在routes中设置一个默认展示{ path: '/', component: app },那么此时index.vue中的router-view是无效
// to="/pageA"和to="pageA"跳转结果是不一致的,'/pageA'是在路由的根目录替换路由,'pageA'是在当前路由添加路由
<template>
<div class="wrap">
<div class="title">
<router-link to="/pageA">Go to pageA</router-link>
<router-link to="/pageB">Go to pageB</router-link>
</div>
<div class="content">
<router-view></router-view>
</div>
</div>
</template>
<script>
export default {
data () {
return {
}
},
computed: {
},
methods: {
},
mounted () {
console.log(this.$router); // 在组件内可以使用this.$router访问路由信息
},
created () {
},
components: {
}
}
</script>
router-link: 是设定路由的导航区域
// 基本参数
to: 路由的指向地址,点击时,默认会把to中参数传给router.push()方法,实现页面跳转
replace: 修改默认跳转的方法,把默认的router.push()替换为router.replace(),这样页面在跳转时,不会有历史记录
tag: router-link默认会渲染为a标签,使用这个参数可以指定渲染为何标签,而且一样会监听点击事件
router-view: 设定路由内容展示的区域
路由传参
在实际开发中,页面通过URL获取一定参数是比较常见的需求,vue-router可以在routes中设定这些信息
// index.js添加一个可以接收相关参数的匹配
const routes = [
{ path: '/pageA', component: pageA },
{ path: '/pageA/:id', component: pageA },
{ path: '/pageB', component: pageB }
];
...
// index.vue添加相关参数
<div class="title">
<router-link to="/pageA">Go to pageA</router-link>
<router-link to="/pageA/900864">Go to pageA ID</router-link>
<router-link to="/pageB">Go to pageB</router-link>
</div>
...
// pageA.vue添加对这个参数的处理
<div>
<p>{{content}} {{ $route.params.id }}</p>
</div>
...
// 在pageA.vue的mounted钩子函数中,查看相关传参数
// this.$route.params输出的就是参数的对象形式,{id: '900864'}
mounted () {
console.log(this.$route.params);
}
...
// params这个信息只在路由匹配时,才会有输出,默认没有值
// 比如这个例子中的pageA和接收参数的pageA,其实调用是同一个组件,当处在不同的URL下,只有在到了含参数的pageA链接时,mounted才会有输出
// 此时你应该留意到了一个问题,就是我们在对pageA和包含id的pageA进行切换时,mounted这个钩子函数只会执行一次
// 那是因为vue处在这种情况下会尽可能的复用当前组件,并不会销毁重建,所以钩子函数只执行一次
// 如果想要对这种变化进行监听,可以使用beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave这几个钩子函数来进行监听
// 代码做如下改造,next方法是必要的,保证路由可以正常调用
// to是要进入路由的信息,from是离开的路由信息
beforeRouteEnter (to, from, next) {
console.log('beforeRouteEnter');
console.log(to);
console.log(from);
next();
},
beforeRouteUpdate (to, from, next) {
console.log('beforeRouteUpdate');
console.log(to);
console.log(from);
next();
},
beforeRouteLeave (to, from, next) {
console.log('beforeRouteLeave');
console.log(to);
console.log(from);
next();
},
这样在进到路由相关页面这几个钩子函数就会执行,执行时机如函数字名的意思
- beforeRouteEnter是进入到路由时执行
- beforeRouteLeave是离开路由时执行
- beforeRouteUpdate是路由更新时执行
beforeRouteEnter, beforeRouteLeave(上一个路由的beforeRouteLeave是下一个路由的beforeRouteEnter)基本一看就知道如何执行,beforeRouteUpdate是在针对同一路由数据更新时触发
// 在index.vue进行如下调整
<router-link to="/pageA">Go to pageA</router-link>
<router-link to="/pageA/900864">Go to pageA ID:900864</router-link>
<router-link to="/pageA/9527">Go to pageA ID:9527</router-link>
<router-link to="/pageB">Go to pageB</router-link>
这样在点击两个pageA ID进行切换时就会发现,只会触发beforeRouteUpdate的钩子函数,官方还提供了watch方法来监听同一路由的变化,估计是之前没有beforeRouteUpdate才有的方法
路由嵌套
Vue本身可以进行多层组件嵌套,路由为了适配这种多层嵌套,路由本身也可以进行嵌套。
// 在项目目录下新建childPageB-A.vue和childPageB-B.vue
// childPageB-A.vue内容如下
<template>
<div>
<p>{{content}}</p>
</div>
</template>
<script>
export default {
data () {
return {
content: '这是页面B的子页面A'
}
}
}
</script>
...
// childPageB-B.vue内容如下
<template>
<div>
<p>{{content}}</p>
</div>
</template>
<script>
export default {
data () {
return {
content: '这是页面B的子页面B'
}
}
}
</script>
...
// index.js路由修改
// 路由中children是用来设置嵌套的子路由,在子路由中path如果添加"/"会被当作根路由
import pageA from './pageA.vue';
import pageB from './pageB.vue';
import pageBA from './childPageB-A.vue';
import pageBB from './childPageB-B.vue';
const routes = [
{ path: '/pageA', component: pageA },
{ path: '/pageA/:id', component: pageA },
{ path: '/pageB', component: pageB, children:[
{ path: 'pageBA', component: pageBA},
{ path: 'pageBB', component: pageBB}
] }
];
...
// index.vue做如下修改,添加到子路由的链接
<router-link to="/pageB">Go to pageB</router-link>
<router-link to="/pageB/pageBA">Go to pageBA</router-link>
<router-link to="/pageB/pageBB">Go to pageBB</router-link>
此时在页面端点击最后两个链接就会发现通过嵌套路由,pageB.vue的组件中也具备路由切换能力
路由命名
路由可以进行多层嵌套而且也可以接受参数,这就意味着在某些情况下路由会变的非常冗长,不如下面这种情况。
// 项目目录下新建childPageA-A.vue,内容如下
<template>
<div>
<p>{{content}} {{ $route.params.info }}</p>
</div>
</template>
<script>
export default {
data () {
return {
content: '这是页面A的子页面A'
}
}
}
</script>
...
// pageA.vue添加路由渲染区域
<p>{{content}} {{ $route.params.id }}</p>
<router-view></router-view>
...
// index.js添加如下内容
// 引入新建组件
import pageAA from './childPageA-A.vue';
...
// pageA路由添加children
{ path: '/pageA/:id', component: pageA, children: [
{path: ':info', component: pageAA}
] },
...
// index.vue添加如下内容
<router-link to="/pageA/9527/info">Go to pageA ID:9527 info:info</router-link>
上例中 to="/pageA/9527/info" 其实并不是一种很好的代码体验,这种代码逻辑给别人看的时候,或者一段时间以后自己会看代码时,根本就弄不清路由中相关参数的意义,针对这种情况,我们可以对路由进行命名,并且通过对象的形式传递相关参数
// 在index.js下代码做如下调整
// 在pageA的子路由添加name属性
const routes = [
{ path: '/pageA', component: pageA },
{ path: '/pageA/:id', component: pageA, children: [
{path: ':info', name: 'pageA', component: pageAA}
] },
{ path: '/pageB', component: pageB, children:[
{ path: 'pageBA', component: pageBA},
{ path: 'pageBB', component: pageBB}
] }
];
...
// index.vue下路由做调整
// 注意这里的name属性设定在子路由中,是因为我们期望的URL是"/pageA/9527/info",params会按'value/value'的形式进行拼接,如果我们这时把name属性定义在page: 'pageA/:id'这一层,会因为没有形参接收info值,忽略info的传参,我们这时把name设定在子路由上,此时完整的路由如:'/pageA/:id/:info'
<router-link :to="{ name: 'pageA', params: { id: '9527', info: 'info'} }">Go to pageA ID:9527 info:info</router-link>
使用这种命名路由可以很清晰的看出我们传递的参数是什么
命名视图
通过上面的例子,我们可以看出,借助router-view我们可以实现,在不同区域展示不同的内容,如果我们在对视图进行命名,可以很容易实现类似iframe的页面组合的效果。
// 项目目录下body.vue和footer.vue
// header.vue内容修改如下
<template>
<div>
<p>{{ title }}</p>
</div>
</template>
<script>
export default {
data () {
return {
title: '这是头部'
}
}
}
</script>
...
// body.vue内容如下
<template>
<div>
<p>{{ content }}</p>
</div>
</template>
<script>
export default {
data () {
return {
content: '这是body'
}
}
}
</script>
...
// footer.vue内容如下
<template>
<div>
<p>{{ footer }}</p>
</div>
</template>
<script>
export default {
data () {
return {
footer: '这是footer'
}
}
}
</script>
...
// index.js引入新建组件
import header from './header.vue';
import body from './body.vue';
import footer from './footer.vue';
...
// index.js路由做相关调整
const routes = [
{ path: '/', components: {
default: pageA,
header: header,
body: body,
footer: footer
}},
...
// index.vue页面做相关调整
<div class="content">
<router-view></router-view>
<router-view name="header"></router-view>
<router-view name="body"></router-view>
<router-view name="footer"></router-view>
</div>
这样在指定的视图中,就会渲染指定内容,如果router-view没有设置name就会默认为default
编程式导航
上面我们都是通过router-link的方式进行页面跳转,除了使用这种方式,我们还可以通过router的相关方法来进行跳转。
router.push(location, onComplete?, onAbort?)
这是插入一条历史记录的跳转方式,类似location.href,参数location是跳转地址,可以是字符形式,也可以是对象形式
// index.vue添加如下代码
<button @click="goPage">Go to pageA</button>
...
// methods下添加goPage方法
goPage () {
// 效果和to="/pageA"一致
// 字符形式
this.$router.push('/pageA');
// 对象形式
// 渲染为/pageA/9527/info
// 要留意使用path时,需不需要添加'/',这个会影响最终渲染的路由路径
// this.$router.push({ name: 'pageA', params: { id: '9527', info: 'info'} });
// 使用对象形式,如果使用了path,就会忽略params属性
// 渲染结果为/pageA
// this.$router.push({ path: '/pageA', params: { id: '9527', info: 'info'} });
// 使用path时,可以使用query属性
// 渲染为/pageA?id=9527&info=info
// this.$router.push({ path: '/pageA', query: { id: '9527', info: 'info'} });
// 对象形式使用name时,params和query都可以使用
// 渲染为/pageA/9527/info?id=9527&info=info
// this.$router.push({ name: 'pageA', params:{id: '9527', info: 'info'}, query: { id: '9527', info: 'info'} });
}
至于onComplete和onAbort是在路由跳转完成和失败时分别执行的回调函数
// 添加onComplete方法
// 此时页面完成跳转后触发onComplete
// onAbort应该是在路由被终止时触发,没想到怎么写这例子...
goPage () {
this.$router.push({ path: '/pageA' }, this.onComplete);
},
onComplete () {
console.log('页面完成跳转')
}
router.replace(location, onComplete?, onAbort?)
使用方式和push一致,区别在于replace不会产生新的浏览器历史记录,和location.replace类似
router.go
和history.go类似,接收一个数字表示进行历史记录前进几个后退几个
重定向
把某个路由指向到指定路由,使用redirect参数设定
// 在index.js的路由中添加如下信息
// 下面表示如果用户访问'/goPageA'会指向'/pageA'
{ path: '/goPageA', redirect: '/pageA' },
...
// index.vue的模板中添加如下信息
<router-link to="/goPageA">Go to goPageA</router-link>
这时当我们点击页面中这个按钮时,页面会自动重定向到'/pageA',重定向还可以使用对象形式,并且可以传参数
// index.js中路由信息修改
// 下面信息重定向到/pageA/9527/info
{ path: '/goPageA', redirect: {name: 'pageA', params: {id: 9527, info: 'info'}} },
别名
把某些比较长的路由信息设定一个访问别名
// 在index.js的路由进行如下修改
// 对'/pageB/pageBB'这个路由设定一个'/BB'的别名
{ path: '/pageB', component: pageB, children:[
{ path: 'pageBA', component: pageBA},
{ path: 'pageBB', component: pageBB, alias: '/BB'}
] }
...
// 在index.vue代码修改如下
<router-link to="/BB">Go to BB</router-link>
这时如果我们点击该按钮,页面会切换到pageBB的组件中,并且URL上路由为'/BB',当我们点击"Go to pageBB"链接,页面URL会变成'/pageB/pageBB',但页面内容不变,比较有意思的是这时vue-router会触发beforeRouteUpdate的钩子函数
组件与路由的解耦
回看我们之前设定的pageA组件
// $route.params.id是从路由上取id参数来使用,这样很方便,但也限制了这个组件的复用
<p>{{content}} {{ $route.params.id }}</p>
...
// 通过以下修改,我们把路由相关的参数提出props
// pageA.vue页面修改如下
<div>
<p>{{content}} {{ id }}</p>
<router-view></router-view>
</div>
...
props: ['id'],
// childPageA-A.vue页面修改如下
<p>{{content}} {{ info }}</p>
...
props: ['info']
...
// index.js中路由做如下修改
// props设为true把$route.params转为了组件属性,
{ path: '/pageA/:id', component: pageA, props: true, children: [
{path: ':info', name: 'pageA', props: true, component: pageAA}
] },
// 如果把props设为一个对象,就会直接像组件传递这个对象中的数据,而忽略传递的参数
// 比如下面这样写,则页面中所有的id都会被设为9876,这个特性在设定一些静态参数时比较好用
{ path: '/pageA/:id', component: pageA, props: { id: 9876 }, children: [
{path: ':info', name: 'pageA', props: true, component: pageAA}
] },
这时我们点击相关链接,发现路由传递的参数依然可以正常使用,而且通过上面的改造,上面的两个组件也与路由进行了解耦,可以当作普通组件使用
导航的跳转监听
vue-router提供了不少钩子函数来监听导航的跳转
在实例化对象上的监听:beforeEach、beforeResolve、afterEach
// 更改index.js路由配置信息
// 添加在路由配置中调用的钩子函数beforeEnter
{ path: '/pageA/:id', component: pageA, props: true, beforeEnter: (to, from, next) => {
console.log('beforeEnter');
next();
}, children: [
{path: ':info', name: 'pageA', props: true, component: pageAA}
] },
...
// 在index.js添加相关钩子函数
const router = new VueRouter({
routes // (简写)相当于 routes: routes
});
// beforeEach是要进入某个路由时最先执行的钩子函数
router.beforeEach((to, from, next) => {
console.log('beforeEach');
console.log(to);
console.log(from);
next();
});
// beforeResolve是在组件路由的钩子函数执行完成后,执行这个钩子函数
router.beforeResolve((to, from, next) => {
console.log('beforeResolve');
console.log(to);
console.log(from);
next();
});
// afterEach是在实例化路由和组件路由都执行完成后,最后执行的钩子函数,这个钩子函数不包含next
router.afterEach((to, from) => {
console.log('afterEach');
console.log(to);
console.log(from);
});
...
// 这时路由相关的钩子函数执行如下
// 进入页面默认执行
beforeEach -> beforeEnter -> beforeRouteEnter -> beforeResolve -> afterEach
...
// 随意切换路由
beforeRouteLeave -> beforeEach -> beforeEnter -> beforeRouteEnter -> beforeResolve -> afterEach
// 在同一级路由切换,这时路由模块并没有切换所以不会触发beforeRouteLeave
beforeEach -> beforeRouteUpdate -> beforeResolve -> afterEach
...
// 按官方文档的解析,整个路由在进行解析时,执行过程如下
1. 导航被触发
2. 如果路由进行切换时是准备进到一个新的路由,并且这个新的路由准备渲染的组件和之前路由的渲染组件不一致,就会尝试去执行旧组件中的beforeRouteLeave函数,如果是在同一个路由下,只是传参不同(比如"/pageA/9527"和"/pageA/900864"),这时并不会执行beforeRouteLeave而是直接执行第3步
3. 尝试执行路由实例化对象beforeEach函数
4-1. 如果路由只是参数不同,这时会尝试执行组件内beforeRouteUpdate函数
4-2-1. 如果路由不同,这时会尝试执行路由配置项中的beforeEnter
4-2-2. 解析异步路由组件(???没看明白指的是什么)
4-2-3. 尝试调用被激活组件的beforeRouteEnter函数
5. 尝试调用路由实例化对象的beforeResolve函数
6. 导航解析完成
7. 尝试调用路由实例化对象的afterEach
8. 触发DOM更新
9. 执行next回调
next用来表示当前路由状态已经被解析,可以继续执行下面的逻辑,next默认不传参,如果传入不同参数,会对路由的执行产生不同的阻断或者跳转。
next(false): 会阻断后续函数执行,并且停留在from路由
next('/') 或 next('{ path: "/" }'): 阻断后续函数执行,并把路由指向要重定向的页面
...
// 在index.js的路由中做如下修改
// 这时你点击与pageA相关的导航,会自动定向到pageB,并且会把路由解析过程重修执行
{ path: '/pageA/:id', component: pageA, props: true, beforeEnter: (to, from, next) => {
console.log('beforeEnter');
next('/pageB');
}, children: [
{path: ':info', name: 'pageA', props: true, component: pageAA}
] },
...
// 在pageA.vue下路由信息添加如下
// next中接收的是一个错误的路由信息
beforeRouteEnter (to, from, next) {
console.log('beforeRouteEnter');
console.log(to);
console.log(from);
next(this.printInfo);
},
...
// 如果你在路由对象实例化时注册了这个函数
let errMsg = function () {
console.log('路由解析发生错误,可以在这里进行异常处理');
};
router.onError(() => {
console.log('onError');
errMsg();
});
meta信息
在定义路由时,可以设置meta信息来设置一些额外的信息。
// 在index.js的定义路由中,设定相关meta信息
{ path: '/pageA', component: pageA, meta: { requiresAuth: true } },
...
// 在pageA.vue中mounted获取定义的meta信息
mounted () {
console.log(this.$route.matched[0].meta);
}
过渡
借用transition标签使router-view在切换内容时,能形成类似app切换的效果
统一过渡效果
// 在index.vue模板中做如下修改
<div class="content">
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
...
// 在index.vue的样式文件中添加如下样式
<style>
.fade-enter-active, .fade-leave-active {
transition: all 1s;
position: relative;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.fade-enter {
left: 50px;
}
.fade-leave-to {
left: -50px;
}
.fade-enter-to, .fade-leave {
opacity: 1;
left: 0;
}
</style>
这样在切换不同组件时就会有app切入切出的效果
不同路由组件的过渡效果
// 清除index.vue下的模板过渡效果
<div class="content">
<router-view></router-view>
...
// pageA.vue下,页面做下面调整
<template>
<transition name="silde">
<div>
...
// pageA.vue下添加下面样式
<style>
.silde-enter-active, .silde-leave-active {
transition: all 1s;
position: relative;
}
.silde-enter {
left: 50px;
}
.silde-leave-to {
left: -50px;
}
.silde-enter-to, .silde-leave {
left: 0;
}
</style>
...
// pageB.vue下添加过渡
<template>
<transition name="opacity">
<div>
...
// pageB.vue添加下面样式
<style>
.opacity-enter-active, .opacity-leave-active {
transition: all 1s;
position: relative;
}
.opacity-enter, .opacity-leave-to {
opacity: 0;
}
.opacity-enter-to, .opacity-leave {
opacity: 1;
}
</style>
这样在切换pageA与pageB的相关路由页面时,会发现pageA与pageB会有不同的过渡效果
滚动行为
在切换页面时,可能会碰到滚动到指定位置的需求,vue-router对此进行了相关封装,使用scrollBehavior使页面滚动到指定位置
// 在index.js中修改代码逻辑
// 在scrollBehavior中return需要跳转的坐标,这样在路由切换时就会跳转到指定位置
const router = new VueRouter({
routes, // (简写)相当于 routes: routes
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
return { x: 0, y: 200 }
}
});
...
// 可以通过promise设置延时滚动
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: 200 })
}, 500)
})
}