官方路由插件,配合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"} |
当改变路径参数时,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 每一次操作都会产生一次新的历史记录
/// 针对上面的内容,有这么个路由配置 { path: '/automobile', component: AutoMobile}, 复制代码
假定访问 /automobile
路由,如果通过 router.push('car'),那路由就会变成 /automobile/car
,如果为 router.push('/bus') ,那么路由就变成 /bus
要注意,以 / 开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。(源于官网)
/// 此处假定当前路由为 /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 复制代码
功能和用法与 push 方法一直,区别在于这个并不会产生历史记录
参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)
对 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'] 复制代码
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的钩子函数分为三个层面
整个路由变化为 /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 信息,在这里可以定义一些信息,供页面中使用,比较直接的用处,就是官网上提到的登陆权限设置
{ 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
进行切换时才会触发过渡,所以添加过渡效果时要明确自己的要触发的路由是哪个
通过之前的钩子函数以及路由参数的分析,我们如果要去请求数据有两种思路来实现:
两者从使用角度来看都没啥问题,按需使用