Vue Router简明教程

官方路由插件,配合Vue使用,将组件 (components) 映射到路由 (routes),然后告诉 Vue Router 在哪里渲染它们

本教程中 vue 是 2.6+,vue-router 3.2+,webpack 4.42+

/// index.html 要有 router-view 标签
/// 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


多段路径参数匹配规则如下

模式 匹配路径 $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

dog: {{kind}} eat: {{eat}}

... 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

...
/// car.vue

...
/// bus.vue

...
/// bicycle.vue 这个是为说明嵌套路由地址的问题

...
/// 在入口路由做如下设置
{
  path: '/automobile/:name', component: AutoMobile, children: [
    {
        path: 'car', component: Car
    },
    {
        path: 'bus', component: Bus
    },
    {
        path: '/bicycle', component: Bicycle
    }
  ]
}

此时如果我们访问 /automobile/byd/bus 或者 /automobile/byd/carautomobile 组件内的 router-view 就会加载相关组件。

相关嵌套的组件的匹配规则可以在 children 中进行设置,这里要留意嵌套路由中 path 的地址,如果匹配路径不加 / ,会如预期一样拼接地址,但上例中的 '/bicycle' 就会匹配到 /bicycle 的路由。

如果我们直接访问 /automobile/byd 此时并没有子路由可以匹配,automobile 组件内的 router-view 不会加载内容,不过如果你想设置一个默认组件,可以匹配空字符串

path: '', component: Car

编程式导航

路由的跳转可以通过暴露的方法,直接进行跳转操作,一般情况下也没必要再组件内设置 来设置页面的跳转,这种方式毕竟不灵活



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

...
/// header.vue

...
/// content.vue

...
/// footer.vue

...
/// 路由设置如下
{ 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 去请求 详情接口,拉取详情页数据,这里是简化


...
/// 路由设置如下
{ path: '/detail/:id', component: Detail},

这里能看出一个问题,就是 detail 组件和路由联系过于紧密,因为我们是从路由页对应的参数上拿初始数据,这就造成了这个组件的通用型不高。

针对此,可以通过路由传参数对组件进行改造

/// 第一步 目标组件与 $route 解藕


...
/// 第二步 在路由参数中设置开启 props 传参形式
{ path: '/detail/:id', component: Detail, props: true}

路由开启 props 传参形式后,需要为当前路由下的命名视图都设置 props 选项

/// header.vue


...
/// footer.vue

/// 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 对应的值都可以直接获取,比如

welcome {{ name }} {{ time }}

... 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 模式下的如预期的执行顺序

分析两个过程,可以看到全局的钩子函数,都会执行,路由上的钩子函数 beforeEnterhash 模式下更新时不会执行,组件的钩子函数,进入时都会执行 beforeRouteEnterbeforeRouteUpdate 只会在 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
  • /
  • /list
  • /parent
  • /parent/foo
  • /parent/bar
... /// list
list
... /// parent.vue ... /// 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 的钩子函数,从中获取相关路由参数,再去获取数据

两者从使用角度来看都没啥问题,按需使用

你可能感兴趣的:(Vue Router简明教程)