Vue Router简明教程
官方路由插件,配合Vue使用,将组件 (components) 映射到路由 (routes),然后告诉 Vue Router 在哪里渲染它们
本教程中 vue 是 2.6+,vue-router 3.2+,webpack 4.42+
/// index.html 要有 router-view 标签
<div id="app">
<router-view></router-view>
</div>
/// index.js
import vue from 'vue';
import VueRouter from 'vue-router';
/*
* 下面是异步组件的写法,使用webpack中的功能,这样是比较合理的拆分,避免页面打包的资源
* 过大
*/
const App = () => import(/* webpackChunkName: "app" */ '../component/main');
const Cat = () => import(/* webpackChunkName: "cat" */ '../component/cat');
const Dog = () => import(/* webpackChunkName: "dog" */ '../component/dog');
vue.use(VueRouter);
const routes = [
{ path: '/cat', component: Cat },
{ path: '/dog', component: Dog },
{ path: '/', component: App},
{ path: '*', redirect: '/' }
] // routes 表示定义的路由
const router = new VueRouter({
routes
}) // router 是vue router的实例
let vm = new vue({
el: '#app',
router,
render: h => h('router-view')
})
{ path: '*', redirect: '/' }
这个匹配中的 * 是通配符路由,会匹配所有路径,所以这个含有通配符的路由应该放在最后,路由 { path: '*' }
通常用于客户端 404 错误。
{ path: '/cat-*', component: Cat }
这也是另外一种通配符,会匹配 /cat-
开头的任意路径,当使用一个通配符时,$route.params 内会自动添加一个名为 pathMatch 参数。它包含了 URL 通过通配符被匹配的部分,比如/cat-black
,对应值为 { pathMatch: "black" }
匹配优先级
如上例,routes 接收的是一个数组,那么这必然涉及到一个匹配的优先级。这里规则很简单,就是谁先定义的,谁的优先级就最高。
动态路由匹配
this.$route 可以访问路由器,params 可以获取路径参数
// 路由参数中的 /:kind 就是一个路径参数
{ path: '/dog/:kind', component: Dog },
...
/// dog.vue
<template>
<div>
<p>dog: {{kind}}</p>
</div>
</template>
<script>
export default {
data () {
return {
kind: ''
}
},
mounted () {
this.kind = this.$route.params.kind // 通过$route.params获取到指定路径参数
}
}
</script>
多段路径参数匹配规则如下
模式 | 匹配路径 | $route.params |
---|---|---|
/dog/:kind | /dog/erha | { kind: "erha" } |
/dog/:kind/eat/:food | /dog/erha/eat/rice | {kind: "erha", food: "race"} |
beforeRouteUpdate
当改变路径参数时,Vue会尽可能的复用组件,减少不必要渲染造成的性能损耗,但这就造成了有些生命周期函数就不会再次被调用,如果在这些生命周期函数中,设置了必须执行的代码,那这就会有问题
/// dog.vue 对应路径参数为 /dog/:kind/eat/:food
<p>dog: {{kind}} eat: {{eat}}</p>
...
data () {
return {
kind: '',
eat: ''
}
},
mounted () {
this.kind = this.$route.params.kind;
this.eat = this.$route.params.food;
},
上述代码中 eat 这个参数时在 mounted 这个钩子函数中设置的,如果在浏览器中把路径从 /dog/erha/eat/rice
改为 /dog/erha/eat/meat
,eat 的值并不会更新,就是因为路径参数变化时,Vue 不会重新触发那些钩子函数,要解决这个问题,只需要使用 beforeRouteUpdate 函数
/// dog.vue
beforeRouteUpdate (to, from, next) {
this.eat = to.params.food;
next();
}
beforeRouteUpdate 一共有三个参数:
参数 | 含义 |
---|---|
to | 即将要进入的目标 路由对象 |
from | 当前导航正要离开的 路由对象 |
next | 后续导航的处理逻辑 |
to、from 就是普通的路由对象,next 会根据其传参不同,功能也不同
参数 | 含义 | 例 |
---|---|---|
() | 不特殊处理,继续进入下一个导航 | next() |
false | 中断导航,如果URL改变,会把URL重置为from路由对应的地址 | next(false) |
'/'或{path: '/'} | 强制跳转到一个指定地址 | next('/'); next({path: '/'}) |
Error实例 | 触发路由实例上的 onError 事件 | next(new Error()) |
当监听了 beforeRouteUpdate ,内部的 next 方法是必须执行的,否则路由行为就会中断
/// index.js
const router = new VueRouter({
routes
})
router.onError(() => {
console.log('error')
})
...
/// dog.vue
beforeRouteUpdate (to, from, next) {
this.eat = to.params.food;
next(new Error()); // 这样当路由更新时会触发绑定的 onError 事件
}
嵌套路由
这是一种动态路由映射不同组件的行为
/// 此时假定有一个查询指定汽车名下车类型的要求
/// automobile.vue
<template>
<div>
名称: {{ $route.params.name }}
<router-view></router-view> // 这里是嵌套路由的入口
</div>
</template>
...
/// car.vue
<template>
<div>car 的特殊参数</div>
</template>
...
/// bus.vue
<template>
<div>bus 的特殊参数</div>
</template>
...
/// bicycle.vue 这个是为说明嵌套路由地址的问题
<template>
<div>这是自行车</div>
</template>
...
/// 在入口路由做如下设置
{
path: '/automobile/:name', component: AutoMobile, children: [
{
path: 'car', component: Car
},
{
path: 'bus', component: Bus
},
{
path: '/bicycle', component: Bicycle
}
]
}
此时如果我们访问 /automobile/byd/bus
或者 /automobile/byd/car
在 automobile 组件内的 router-view
就会加载相关组件。
相关嵌套的组件的匹配规则可以在 children 中进行设置,这里要留意嵌套路由中 path 的地址,如果匹配路径不加 / ,会如预期一样拼接地址,但上例中的 '/bicycle'
就会匹配到 /bicycle
的路由。
如果我们直接访问 /automobile/byd
此时并没有子路由可以匹配,automobile 组件内的 router-view
不会加载内容,不过如果你想设置一个默认组件,可以匹配空字符串
path: '', component: Car
编程式导航
路由的跳转可以通过暴露的方法,直接进行跳转操作,一般情况下也没必要再组件内设置 <router-link :to="...">
来设置页面的跳转,这种方式毕竟不灵活
<template>
<div>
名称: {{ $route.params.name }}
<router-view></router-view>
<button @click="goToPage('car')">car</button>
<button @click="goToPage('bus')">bus</button>
</div>
</template>
<script>
export default {
methods: {
goToPage (val) {
this.$router.push(val)
}
}
}
</script>
route 是当前路由的基本信息
命名路由
就是对一个路由,提供一个别名快速访问,这个别名用在控制页面跳转时作为特别参数使用
path: '/automobile/:name', component: AutoMobile, name: 'automobile',
router.push(location, onComplete?, onAbort?)
router.push 每一次操作都会产生一次新的历史记录
location 可以是一个字符串,也可以是一个对象
- 当为字符串时,就是在当前路由下按规则拼接子路由
/// 针对上面的内容,有这么个路由配置
{ path: '/automobile', component: AutoMobile},
假定访问 /automobile
路由,如果通过 router.push('car'),那路由就会变成 /automobile/car
,如果为 router.push('/bus') ,那么路由就变成 /bus
要注意,以 / 开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。(源于官网)
- 当为对象时,这个对象可以设置四个属性,path、params、query和name
- path 和 params 参数不共存,path 可以搭配 query 一起使用
- params 可以和 name 一起使用
/// 此处假定当前路由为 /automobile/byd 组件内容如本节中有两按钮,通过 goToPage 设置页面的跳转
...
/// 单独使用 path
router.push({
path: 'car'
}) // /automobile/car
...
/// 单独使用 path 但是路径写完整可以跳到任何路径
router.push({
path: '/automobile/byd/car?color=red'
})
...
/// 使用 path 和 query
router.push({
path: 'car',
query: {
color: 'red'
}
}) // /automobile/car?color=red
...
/// 使用 name 和 params,注意上一小节对路由的命名
router.push({
name: 'automobile',
params: {
name: 'byd'
}
}) // /automobile/:name 这种相当于直接设置了name值,最终路径为 /automobile/byd
/// 如果定义路由时没有可以接受的参数,就会忽略 params 的值
{ path: '/list', component: List, name: 'automobile' },
这时跳转后路径直接为 /list
router.replace(location, onComplete?, onAbort?)
功能和用法与 push 方法一直,区别在于这个并不会产生历史记录
router.go(n)
参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)
- router.go(1) 相向一步,router.go(-1) 后退一步
- router.go(-100) 如果历史记录并没有这么多条,就会失败
命名视图
对 router-view 进行命名,多个命名的视图可以组成各种复杂的布局
/// index.vue
<template>
<div>
<router-view name='header'></router-view>
<router-view name='body'></router-view>
<router-view name='footer'></router-view>
</div>
</template>
...
/// header.vue
<template>
<div>Header</div>
</template>
...
/// content.vue
<template>
<div>Content</div>
</template>
...
/// footer.vue
<template>
<div>Footer</div>
</template>
...
/// 路由设置如下
{ path: '', component: Index, children: [
{
path: '/index', components: { // 注意如果要使用多个组件要使用 components
header: Header,
body: Body,
footer: Footer
} // 这样就对每个命名视图,设置了载入的组件
}
]},
在使用命名视图时,要考虑好组件最终会渲染在哪个视图上,上面设定的 components 是基于 index 组件,在组件内是有这三个命名视图,所以页面可以正常加载组件内容
/// 如果这样设置,在根元素,只有一个默认的视图,就不能正常渲染
{
path: '/index', alias: '/indexs', components: {
header: Header,
body: Body,
footer: Footer
}
}
重定向和别名
重定向
{ path: '/indexs', redirect: '/index'},
/// 通过 redirect 设置重定向的路径,当访问 /indexs 会直接重定向为 /index 这期间并不会产生浏览器记录
别名
path: '/index', alias: '/indexs',
/// alias 就是给路由设定一个别名,访问 /indexs 和 /index 是一样
路由组件的传参
/// detail.vue
/// 正常情况下,detail 页面会拿到 id 去请求 详情接口,拉取详情页数据,这里是简化
<template>
<div>
详情页编号:{{ $route.params.id }}
</div>
</template>
<script>
export default {
}
</script>
...
/// 路由设置如下
{ path: '/detail/:id', component: Detail},
这里能看出一个问题,就是 detail 组件和路由联系过于紧密,因为我们是从路由页对应的参数上拿初始数据,这就造成了这个组件的通用型不高。
针对此,可以通过路由传参数对组件进行改造
/// 第一步 目标组件与 $route 解藕
<template>
<div>
详情页编号:{{ id }}
</div>
</template>
<script>
export default {
props: ['id'] // 设定在 props 中,这更像是一个普通组件
}
</script>
...
/// 第二步 在路由参数中设置开启 props 传参形式
{ path: '/detail/:id', component: Detail, props: true}
路由开启 props 传参形式后,需要为当前路由下的命名视图都设置 props 选项
/// header.vue
<template>
<div>Header <p>welcome {{ name }}</p></div>
</template>
<script>
export default {
props: ['name']
}
</script>
...
/// footer.vue
<template>
<div>Footer @ {{ $route.params.time}}</div>
</template>
/// footer 和 header 此处使用了两种不同方式获取参数
...
/// 配置信息如下
{ path: '/detail', component: Detail, children:[
{
path: ':id/:name/:time', components: {
header: Header,
footer: Footer
}, props: {
header: true // 这里设置 header 使用 props 的方式获取参数
}
}
], props: true},
/// 假定访问路径为 /detail/23/rede/2020 对应的params值为
{id: "23", name: "rede", time: "2020"}
...
/// 启用了 props 方式,params 对应的值都可以直接获取,比如
<p>welcome {{ name }} {{ time }}</p>
...
props: ['name', 'time']
HTML5 History 模式
Vue-router 默认情况下,路由是hash模式,也就是页面路径会是 http://localhost:8181/#/detail/23/rede/2020
,这里一直使用 webpack-dev-server 起的服务,在对应配置信息为
devServer: {
port: 8181
},
这种hash模式看起来不简洁,可以开启 history 的模式,做如下配置
/// webpack.config.js
output: {
filename: '[name].js',
publicPath: '/' // 记得设置查找路径,这表示在引入静态资源时,从根路径开始引入
},
devServer: {
port: 8181,
historyApiFallback: true // 开启history模式
},
...
/// index.js
const router = new VueRouter({
mode: 'history',
routes
})
...
/// 这样访问路径就是 http://localhost:8181/detail/23/rede/2022
webpack-dev-server 内部其实就是使用官方文档中推荐的 connect-history-api-fallback 然后通过暴露的方法进行调用
相关钩子函数
VueRouter的钩子函数分为三个层面
- 全局钩子函数:beforeEach、beforeResolve、afterEach
- 路由钩子函数:beforeEnter
- 组件钩子函数:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
|函数名|参数|执行时期|备注|
|---|---|---|---|
|beforeEach|(to, from, next)|全局||
|beforeResolve|(to, from, next)|全局|会在路由和组件内的钩子函数执行后调用|
|afterEach|(to, from)|全局|无需next参数||
||||||
|beforeEnter|(to, from, next)|路由||
|||||
|beforeRouteEnter|(to, from, next)|组件|组件实例还没被创建,再其内部不可通过this获取当前组件内的数据|
|beforeRouteUpdate|(to, from, next)|组件|在当前路由改变,但是该组件被复用时调用,此时可以调用this获取组件内数据|
|beforeRouteLeave|(to, from, next)|组件|导航离开该组件的对应路由时调用,可以通过this获取组件内数据|
钩子函数的执行顺序
整个路由变化为 /list 变为 /detail/23/rede/2020 再到 /detail/23/rede/2021 的演变程度
-
hash模式
|路由变化|钩子函数执行顺序|
|---|---|
|进入/list
| beforeEach -> beforeEnter -> beforeRouteEnter -> beforeResolve -> afterEach|
|从/list
到/detail/23/rede/2020
|beforeRouteLeave (list组件中的钩子函数) -> beforeEach -> beforeEnter -> beforeRouteEnter -> beforeResolve -> afterEach|
|从/detail/23/rede/2020
到/detail/23/rede/2021
|beforeEach -> beforeRouteUpdate -> beforeResolve -> afterEach| -
history模式
|路由变化|钩子函数执行顺序|
|---|---|
|进入/list
| beforeEach -> beforeEnter -> beforeRouteEnter -> beforeResolve -> afterEach|
|从/list
到/detail/23/rede/2020
|beforeEach -> beforeEnter -> beforeRouteEnter -> beforeResolve -> afterEach|
|从/detail/23/rede/2020
到/detail/23/rede/2021
|beforeEach -> beforeEach -> beforeRouteEnter -> beforeResolve -> afterEach |
在 history 模式下组件内的钩子函数 beforeRouteLeave 不会执行,而且同路由切换参数时,钩子函数也是重新执行,并不是 hash 模式下的如预期的执行顺序
分析两个过程,可以看到全局的钩子函数,都会执行,路由上的钩子函数 beforeEnter 在 hash 模式下更新时不会执行,组件的钩子函数,进入时都会执行 beforeRouteEnter ,beforeRouteUpdate 只会在 hash 模式下更新参数执行,beforeRouteLeave 只会在 hash 模式下,切换路由时执行
路由的 meta 信息
路由在定义时可以设置 meta 信息,在这里可以定义一些信息,供页面中使用,比较直接的用处,就是官网上提到的登陆权限设置
{ path: '/list', component: List, meta: { requiresAuth: true } },
{ path: '/detail', component: Detail, children:[
{
path: ':id/:name', components: {
header: Header
}, props: {
header: true
},
meta: { requiresAuth: true },children: [{
path: ':time',
components: {
header: Header,
footer: Footer
}
}]
}
], props: true, meta: { requiresAuth: false }}
...
/// index.js
router.beforeEach((to, from, next) => {
console.log(to.matched);
next()
})
$route.matched
会返回路由的信息,这其中就包含 meta 的信息,这是个数组,返回数组数按子路由进行拆分,比如 /list
返回的数组就是一条,而如果按例子配置,访问 /detail/23/rede/2020
,那返回的数据就是三条,对应path 分别为
{path: "/detail" ...
{path: "/detail/:id/:name" ...
{path: "/detail/:id/:name/:time" ...
路由上设定的 meta 信息就是在这里的对象上,通过获取这个可以控制权限或者别的什么
过渡动效
VueRouter 的路由切换是基于对router-view 内容的修改实现的,整体感觉很像是动态组件的感觉,可以使用过渡组件,在路由切换时添加上一些动效
/// 下面例子,参考官网,稍改
/// app.vue
<div id="app">
<ul>
<li>
<router-link to="/">/</router-link>
</li>
<li>
<router-link to="/list">/list</router-link>
</li>
<li>
<router-link to="/parent">/parent</router-link>
</li>
<li>
<router-link to="/parent/foo">/parent/foo</router-link>
</li>
<li>
<router-link to="/parent/bar">/parent/bar</router-link>
</li>
</ul>
<transition name="fade" mode="out-in">
<router-view class="view"></router-view>
</transition>
</div>
...
/// list
<div>list</div>
...
/// parent.vue
<template>
<div class="parent">
<h2>Parent</h2>
<transition :name="transitionName">
<router-view class="child-view"></router-view>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
transitionName: "slide-left"
};
},
beforeRouteUpdate(to, from, next) {
const toDepth = to.path.split("/").length;
const fromDepth = from.path.split("/").length;
this.transitionName = toDepth < fromDepth ? "slide-right" : "slide-left";
next();
}
};
</script>
...
/// index.js
routes: [
{ path: "/", component: App },
{ path: "/list", component: List },
{
path: "/parent",
component: Parent,
children: [
{ path: "", component: Default },
{ path: "foo", component: Foo },
{ path: "bar", component: Bar }
]
}
]
...
/// 样式如下
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.view {
position: relative;
}
.child-view {
position: absolute;
width: 100%;
transition: all 0.8s ease;
top: 40px;
}
.slide-left-enter, .slide-right-leave-active {
opacity: 0;
transform: translate(100%, 0);
}
.slide-left-leave-active, .slide-right-enter {
opacity: 0;
transform: translate(-100% 0);
}
这里一共用到了两个过渡组件一个是在 app.vue 组件中的 transition name="fade"
,另一个是在 parent.vue 中的 transition :name="transitionName"
,按照动态组件的思想来思考,只有在对应包裹的 router-view
进行切换时才会触发过渡,所以添加过渡效果时要明确自己的要触发的路由是哪个
数据获取时机
通过之前的钩子函数以及路由参数的分析,我们如果要去请求数据有两种思路来实现:
- 无视路由提供的钩子函数,完全依赖组件内的生命周期函数(比如 mounted),通过获取路由参数($route.params),在组件内合适的生命周期中触发获取数据的方法
- 利用路由提供的钩子函数,在组件内调用,比如 beforeRouteEnter 的钩子函数,从中获取相关路由参数,再去获取数据
两者从使用角度来看都没啥问题,按需使用