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/carautomobile 组件内的 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>

router** 是对应 VueRouter 的实例对象,**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 模式下的如预期的执行顺序

分析两个过程,可以看到全局的钩子函数,都会执行,路由上的钩子函数 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
<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 的钩子函数,从中获取相关路由参数,再去获取数据

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349