人家都在玩源码,你还在纠结vue-router的使用...

710 阅读4分钟

来吧废话少说,我们争取一篇文章就搞定它的使用及原理

先来复习一下vue的小兄弟萌吧(用法主要提炼的官网)

最近一直在受虐,状态不是很好了。如有误,望指教,感恩!

姐妹篇 vuex的使用及手写

一、vue-router(hash模式)

1. 从使用开始

1.1 基础

1.1.1 动态路由

动态路由即:像这样good/001,good/002的这样的路径都能映射于同一组件

配置路由写法

[
    {
        path: '/good/:id',
    }
]

路径匹配到这个路由时,:后面的参数被放到当前组件实例的$router.params中,即我们可以在组件中获取当前路径的:后的参数

栗子:(注意根组件给该组件的显示留坑)

<template>
    <div>
        Good
       {{$route.params.id}}
    </div>
</template>

当然,很多时候获取url中传递的参数也是很有必要的(?后面)。

<template>
    <div>
        Good
       {{$route.params.id}}
       {{$route.query}}
       
    </div>
</template>

1.1.2 嵌套路由

即如我们上面一直待在商品页面,现在我们要去商品详情

它的路径假设是...good/01/details

const routes = [{
    path: '/good/:id',
    component: Good,
    children: [{
        path: 'details',
        component: Details
    }]
}]

1.1.3 编程式导航

一般在组件中,我们是拿router提供的<router-link>充当a链接的,它在模板中是比较好使,但是js中呢?

这时我们就要用到一个API=>this.$router.push

push方法,见名知意一般以它命名的都是一个推栈操作。这里也不例外,它会向history栈中添加一个新的记录。

可以这样理解,有这样一个url的历史记录栈

匹配到一个路由,就向历史栈内推入它的地址。那么点击返回键的时候自然就返回到上一个页面了

其实<router-link>内部也是调用了这个API,它们是等价的

栗子:

 this.$router.push('good/1')   //http://localhost:8080/#/good/1
 this.$router.push({path:'/good/2'})
//http://localhost:8080/#/good/2


 this.$router.push({name:'good',params:{id:3}})
//在路由规则配置中添加一条name
const routes = [{
    name: 'good',
    path: '/good/:id',
    component: Good,
    children: [{
        path: 'details',
        component: Details
    }]
}]
//http://localhost:8080/#/good/3

值得注意的是:如果提供了path,那么params就会被忽略

再来看两个API

router.replace

router.push是在history中又添加一条记录,并调转。而router.replace是替换当前history栈顶记录。

它们区别就是使用push方法跳转之后,点击返回还能返回;replace而已经彻底失去它了

  this.$router.replace({path:'/good02'})
router.go(n)

这个比较简单了,顺着history向前或向后走多少步(到头就不走了)

this.$router.go(1)
this.$router.go(-1)

1.1.4 命名路由与命名视图

命名路由

命名路由就是配置规则时,给这个路由搞个名字(刚才已经写了栗子)

const routes = [{
    name: 'good',
    path: '/good/:id',
    component: Good,
    children: [{
        path: 'details',
        component: Details
    }]
}]

组件中的两种匹配

<router-link :to="{name:'good',params:{id:4}}">跳转</router-link>

this.$router.push({name:'good',params:{id:5}})

命名视图

所谓命名视图就是给指给<router-view></router-view>这个占坑的小家伙一个name属性,使它变得不那么随便了,只为同名的组件占

官方的意思是说:我们都知道<router-view></router-view>,这一个坑位只能占一个组件,但是很多情况下这一个坑位是不够用的。

比如我匹配到index路由时,这个index页面最起码得有一个头部和主体吧。那么就是说我们至少得在它的父组件中放两个坑位

来直接看栗子

 {
        path: '/',
        components: {
            test01: Test01,//键是坑位名字
            test02: Test02,
            defaultTest03//没有名字就是default,匹配到没有name属性的坑位中
        }
    }

其组件坑位就可以使用name来指定了

<router-view name="test01"></router-view>
<router-view name="test02"></router-view>
<router-view />

1.1.5 重定向与别名

比较简单直接看栗子吧

重定向
//最简单的
{ path: '/good', redirect: '/'component: Good}

//也可以指定名字
{ path: '/good', redirect: { name: 'index' },component: Good}

//也可以是一个方法,接收一个目录路由作为参数
{ path: '/good',   redirect: to => {
        const { params } = to
        if (params.id === 1) {
            return '/'
        }

    },component: Good}
别名
 { path: '/good', alias: '/goodAlias'component: Good }

1.1.6 路由组件传参

前面我们是怎么传参的呢?

我们是利用动态路由,和route.params

{
     path: '/good/:id',
     component: Good,
}
{{$route.params.id}}

但是:

那么来看一下新的方式吧

配置中新加一个props属性,注意props的值不止可以是布尔

{
        name: 'good',
        path: '/good/:id',
        component: Good,
        props: true
    },

组件中,使用父子通信的方式。放进props中

<template>
    <div>
      商品{{id}}
      <router-view/>
    </div>
</template>
<script>
export default {
    props:['id']
}
</script>

1.2 守卫

1.2.1 全局守卫

全局前置守卫

每个守卫方法接收三个参数

  • to:即将要进入的路由

  • from:当前离开的路由

  • next:它是一个方法,必须要调用。它有以下参数

    • 不传,即next()。比较当前钩子执行完,进入下一个钩子(如果有的话)

    • next('/')等价月next({path:'/'}),类似于编程式导航,转到根路径下(或别的地址)

    • next(error),(注意这个error是一个Error的实例) 则导航会被终止且该错误会被传递给 router.onError()注册过的回调。

来一个栗子,不允许进入good/1

router.beforeEach((to, from, next) => {
    if (to.name === 'good' && to.params.id === '1') {
        console.log('这个地方你不能来');
        next({ path: '/' })
    } else {
        next()
    }
})
全局后置钩子

与前置守卫不同的是,它的方法里不接收next函数

router.afterEach((to, from) => {
  // ...
})

1.2.2 路由独享守卫

在配置路由规则时,直接定义

{
        name: 'good',
        path: '/good/:id',
        component: Good,
        props: true,
        beforeEnter: (to, from, next) => {
            if (to.name === 'good' && to.params.id === '2') {
                console.log('2也不行')
                next({ path: '/' })
            } else {
                next()
            }

        }
    },

1.2.3 组件内的守卫

beforeRouteEnter
<template>
    <div>
   
    </div>
</template>
<script>
export default {
    beforeRouteEnter (to, from, next) {
        if(to.name==='good'&&to.params.id==='3'){
            console.log('3也不行')
            next({path:'/'})
        }else{
            next()
        }
    }
}
</script>

这里需要注意的是:

这个方法是在导航被确认前调用,即没有还没进入当前路由。那么当前路由所对应的组件也还没有被创建,故在此方法中拿不到组件实例

但是,若你非要在此方法中拿当前组件实例。也是有办法的

可以给next方法传一个回调,这个回调函数的参数就是当前组件实例。当然因为能拿到实例,那么这个回调方法肯定是在这导航确认的时候调用的。这个回调的返回值就被当做next的参数

栗子

beforeRouteEnter (to, from, next) {
       next(vm=>{
           console.log(vm);
       })
    }

只要这个守卫的next方法支持传递回调,下面两个方法被调用时组件早已被创建了,也就不用再这么麻烦的去取实例了。下面两个就用官网的栗子了

beforeRouteUpdate
beforeRouteUpdate (to, from, next) {
  this.name = to.params.name
  next()
}
beforeRouteLeave
beforeRouteLeave (to, from, next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}
导航流程是蛮重要的

来看一些这些钩子的调用顺序

验证一下吧

上面是从一个路由转到另一路由的情况,所以开始就是组件内的离开钩子。

这里为了简单,我以直接进入一个路由为栗子

先把钩子都设好

//配置中
{
        name: 'good',
        path: '/good/:id',
        component: Good,
        props: true,
        beforeEnter: (to, from, next) => {
            console.log('配置中beforeEnter')
            next()

        }
    },
//全局钩子
  
router.beforeEach((to, from, next) => {
    console.log('全局守卫')
    next()
})
router.beforeResolve((to, from, next) => {
    console.log('全局解析')
    next()
})
router.afterEach((to, from) => {
    console.log('全局后置' + to + from)//这里我也不想打印后面的to,不打印eslint过不了
})      

//组件内
 beforeRouteEnter(to, from, next) {
    console.log("组件内前置");
    next((vm) => {
      console.log("组件内前置回调");
      console.log(vm);
    });
  },
  beforeRouteUpdate(to, from, next) {
    console.log("组件内更新");
    next();
  },
  beforeRouteLeave(to, from, next) {
    console.log("组件内后置");
    next();
  },

开始

url由http://localhost:8080/#/good/2,改为 http://localhost:8080/#/good/3。为触发beforeRouteUpdate

全局解析钩子前面没有介绍,它和全局守卫一样只是如上图的调用时机的区别

1.3 再来解释一些别的常用知识

1.3.1 路由懒加载

即传统的写法,是不管这个组件定义有没有用到都是先导进来再说。这会加大打包的体积(因为有些可能就没有用到)

懒加载写法:

{
        name: 'good',
        path: '/good/:id',
        component: () =>
            import ('../components/test01.vue'),
        // props: true,
        // beforeEnter: (to, from, next) => {
        //     console.log('配置中beforeEnter')
        //     next()

        // }
    },

1.3.2 其他小知识

像数据获取,和滚动行为可简单看一下官网吧

数据获取的时机,无论是进入这个路由后在当前组件的生命周期钩子中拿还是进入这个路由之前在路由守卫中拿我们平常都使用过

其这个滚动行为的触发是得 通过浏览器的 前进/后退 按钮才可以,有一点点价值吧,不是很重要

2. 实现一个简易版的路由吧

先来分析一下路由的配置文件中做了什么

首先Vue.use(VueRouter),说明VueRouter是一个插件。故它内部(对象的话)需要提供一个install方法(若是函数,这个函数就被作为install)。同时内部的这个install方法被调用时,会将Vue作为参数传入

下面创建了一个路由配置规则,它是一个数组,在new VueRouter时作为构造参数传入

再来看看入口文件main.js

将上面实例好的一个路由对象,作为选项放进new Vue的构造参数选项中

开整

class MyRouter {
    constructor(options) {
        this.$options = options
    }
}

//需要实现一个install方法
MyRouter.install = function(vue) {
    
}

install方法中我们要做的工作就是在new Vue时,将我们路由配置时搞的路由实例放进Vue的原型上

这里肯定是使用混入了,但是我们仅是想在根实例创建的时候把路由实例绑到Vue原型上,那么现在就得比较根实例和普通组件实例的区别了。

区别一眼就可以看出了,根实例的构造参数选项中有一个router对象,也就是我们的router实例

那么我们就可以在一个生命周期钩子中,用$options.router取到这个router实例并放进vue原型

 vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                vue.prototype.$router = this.$options.router
            }
        },
    })

创建两个路由组件router-linkrouter-view

注意只有在编译器的版本下才可以使用模板语法,这里只能使用jsx或h函数

router-link,就是创建一个a链接。它有个特性href,故需要父组件传参。同时插槽的内容也是不可少的

vue.component('router-link', {
        props: ['to'],
        render(h) {
            return h('a', {
                attrs: {
                    href: '#' + this.to
                }
            }, this.$slots.default)
        },
    })

router-view,就是在url的hash值变化之后,重新拿新的hash值和路由配置中的个个path进行匹配,匹配到了就把它对应的component渲染出来

拿数据时可能很长但是只有这样一步步拿了,组件实例先拿到router实例(上一步刚放vue原型上),router实例再拿它的$options(构造函数已传),到最后拿到路由规则routes

值得注意的是,this.$router.current.current这个hash值之所以写的这么丑了写,是因为这个数据是要响应式的。因为我们要保证每次url的hash变动时这个函数要跟着调用,派发更新的活我就直接扔给vue了

 vue.component('router-view', {
        render(h) {
            let component = null

            this.$router.$options.routes.forEach(route => {
                if (route.path === this.$router.current.current) {
                    console.log(111)
                    component = route.component
                }
            })
            return h(component)
        }
    })

保存hash值,监听hash改变事件

注意上面可以拿到vue,是通过install方法的参数传过来的,MyRouter类这里可是拿不到的,

先声明一个变量Vue,在install的时候

响应式的问题,我们利用了Vue的data选项,data选项的数据是会在初始化时响应式化过的(不考虑数组)

再一个简单的注意问题就是 window.addEventListener('hashchange', this.onHashChange.bind(this))

不绑this的话,显然this.onHashChange是一个默认绑定this指向undefined(严格模式),那么下面的onHashChange方法就无法通过this.current.current存储哈希值了

let Vue
class MyRouter {
    constructor(options) {
        this.$options = options
        let currentHash = window.location.hash.slice(1) || '/'
        this.current = new Vue({
            data: {
                current: currentHash
            }
        })

        window.addEventListener('hashchange', this.onHashChange.bind(this))
    }
    onHashChange() {
        this.current.current = window.location.hash.slice(1)
    }

}

到此一个简单的路由就完成了

写到最后

希望这篇文章能给与您一丝帮助(卑微求赞)

期待着我们的下一次邂逅