Vue现代化使用方法(五)--vue-router

vue-router是vue全家桶中,用来控制路由的插件

安装

依然使用npm进行安装

npm install vue-router --save

基本使用方式

// 在项目目录建立两个文件pageA.vue和pageB.vue
// pageA.vue内容如下


...
// pageB.vue内容如下


...
// 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'是在当前路由添加路由


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添加相关参数
Go to pageA Go to pageA ID Go to pageB
... // pageA.vue添加对这个参数的处理

{{content}} {{ $route.params.id }}

... // 在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进行如下调整
Go to pageA
Go to pageA ID:900864
Go to pageA ID:9527
Go to pageB

这样在点击两个pageA ID进行切换时就会发现,只会触发beforeRouteUpdate的钩子函数,官方还提供了watch方法来监听同一路由的变化,估计是之前没有beforeRouteUpdate才有的方法

路由嵌套

Vue本身可以进行多层组件嵌套,路由为了适配这种多层嵌套,路由本身也可以进行嵌套。

// 在项目目录下新建childPageB-A.vue和childPageB-B.vue
// childPageB-A.vue内容如下


...
// childPageB-B.vue内容如下


...
// 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做如下修改,添加到子路由的链接
Go to pageB
Go to pageBA
Go to pageBB

此时在页面端点击最后两个链接就会发现通过嵌套路由,pageB.vue的组件中也具备路由切换能力

路由命名

路由可以进行多层嵌套而且也可以接受参数,这就意味着在某些情况下路由会变的非常冗长,不如下面这种情况。

// 项目目录下新建childPageA-A.vue,内容如下


...
// pageA.vue添加路由渲染区域

{{content}} {{ $route.params.id }}

... // index.js添加如下内容 // 引入新建组件 import pageAA from './childPageA-A.vue'; ... // pageA路由添加children { path: '/pageA/:id', component: pageA, children: [ {path: ':info', component: pageAA} ] }, ... // index.vue添加如下内容 Go to pageA ID:9527 info:info

上例中 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'
Go to pageA ID:9527 info:info

使用这种命名路由可以很清晰的看出我们传递的参数是什么

命名视图

通过上面的例子,我们可以看出,借助router-view我们可以实现,在不同区域展示不同的内容,如果我们在对视图进行命名,可以很容易实现类似iframe的页面组合的效果。

// 项目目录下body.vue和footer.vue
// header.vue内容修改如下


...
// body.vue内容如下


...
// footer.vue内容如下


...
// 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页面做相关调整

这样在指定的视图中,就会渲染指定内容,如果router-view没有设置name就会默认为default

编程式导航

上面我们都是通过router-link的方式进行页面跳转,除了使用这种方式,我们还可以通过router的相关方法来进行跳转。

router.push(location, onComplete?, onAbort?)

这是插入一条历史记录的跳转方式,类似location.href,参数location是跳转地址,可以是字符形式,也可以是对象形式

// index.vue添加如下代码

...
// 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的模板中添加如下信息
Go to goPageA

这时当我们点击页面中这个按钮时,页面会自动重定向到'/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代码修改如下
Go to BB

这时如果我们点击该按钮,页面会切换到pageBB的组件中,并且URL上路由为'/BB',当我们点击"Go to pageBB"链接,页面URL会变成'/pageB/pageBB',但页面内容不变,比较有意思的是这时vue-router会触发beforeRouteUpdate的钩子函数

组件与路由的解耦

回看我们之前设定的pageA组件

// $route.params.id是从路由上取id参数来使用,这样很方便,但也限制了这个组件的复用 

{{content}} {{ $route.params.id }}

... // 通过以下修改,我们把路由相关的参数提出props // pageA.vue页面修改如下

{{content}} {{ id }}

... props: ['id'], // childPageA-A.vue页面修改如下

{{content}} {{ info }}

... 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模板中做如下修改
... // 在index.vue的样式文件中添加如下样式

这样在切换不同组件时就会有app切入切出的效果

不同路由组件的过渡效果

// 清除index.vue下的模板过渡效果
... // pageA.vue下,页面做下面调整