vue-router

736 阅读6分钟

vue-router

起步

// 模板中
<template>
    <p>
        // 使用router-link 来导航
        // to指定连接
        // <router-link> 默认渲染成一个a标签
        <router-link to="/foo">Foo</router-link>
        <router-link to="/bar">Bar</router-link>
    </p>
    <router-view></router-view>
</template>

// router/index.js
0:首先导入Vue和VueRouter 先用Vue.use(VueRouter);
1:声明路由组件,在router/index.js ,import导入
2:定义路由

// 根实例上通过router配置参数注入路由,从而让整个应用都有路由功能
let vm = new Vue({
    router
})

// 创建router实例
传入routes配置参数
const router = new VueRouter({
    routes
})

//每个路由映射一个组件
const routes = [
    {
        path:'/foo',
        component:Foo
    },
    {
        path:'/bar',
        component:Bar
    }
]

通过注入路由器,我们可以在任何组件内通过this.$router访问路由器,也可以额通过this.$routes访问当前路由

this.$router:访问路由器 this.$routes:访问当前路由

export default{
    computed:{
        username(){
            return this.$route.params.username
        }
    },
    methods:{
        goBack(){
            window.history.length>1?this.$router.go(-1):this.$router.push('/')
        }
    }
}

<router-link></router-link>匹配到对应的路由时候,会自动设置.router-link-active类名

动态路由匹配

我们经常需要把某种模式匹配到所有路由,全都映射到同个组件。 比如一个User组件,对于ID不用的用户,都要使用这个组件来渲染、 那么我么那就可以使用动态路径参数

// router/index.js
{
    path:'/user/:id',
    component:User
}

// User.vue
<template>
    <div id="user">
        USER:{{ $route.params.id}}
    </div>
</template>

// 如果不传id的话不会展示当前User模板
传递参数会被设置到`this.$route.params`,可以在每个组件内使用。
回去User模板,输出当前的id`{{$route.params.id}}`

一个路由中设置多段路径参数,对应的值都会设置到$route.params

/user/:username /user/evan {username:'evan'} /user/:username/post/:post_id /user/evan/post/123 {username:'evan',post_id:'123'} 通过$route.params获取 $route.params.query:获取URL查询参数 $route.hash

响应路由参数的变化

/user/foo 导航到/user/bar 原理的组件实例会被复用。 因为两个路由都渲染了同一个组件,意味着组件的生命周期钩子不会被调用。

复用组件时,要想对路由参数的变化做出响应,可以简单的watch$route 对象

const User = {
    template:'...',
    watch:{
        '$route'(to,from){
            //对路由变化做出响应
        }
    }
}
也可以使用`beforeRouteUpdate`
const User = {
    template:'...',
    beforeRouteUpdate(to,from,next){
        // ...
    }
}

捕获所有路由或404路由

const route = [
    {
        //会匹配所有路由
        path:'*'
    },
    {
        //会匹配以`user-`开头的任意路径
        path:'/user-*'
    }
]

含有通配符*的路由应该放到最后 路由{path:'*'}通常用于客户端404错误

当使用一个通配符时,$route.params内会自动添加一个名为pathMatch参数。它包含了URL通过通配符被匹配的部分

//给出一个路由 {path:'/user-*'}
this.$route.params.push('/user-admin')
this.$route.params.pathMatch  //'admin'

//给出一个路由 {path:'*'}
this.$route.push('/no-existing')
this.$route.params.pathMatch  // '/no-existing'

匹配优先级

同一个路径可以匹配多个路由, 此时匹配的优先级就按照路由定义顺序:谁先定义的,谁的优先级最高

嵌套路由

实际应用界面,通常都是多层嵌套的组件组合而成 URL中各段动态路径也按某种结构对应嵌套的的各层组件

<div id="app">
    <router-view></router-view>
</div>

const User = {
    template:'<div>User,{{$route.params.id}}</div>'
}

const router = new VueRouter({
    routes:[
        path:'/user/:id',
        component:User
    ]
})

这里的<router-view>是最顶层的出口,渲染最高级路由匹配到的组件。 同样一个被渲染的组件可以包含自己嵌套的<router-view>

const User = {
    template:'<div class="user">
        <h2>User{{ $router.params.id}}</h2>
        <router-view></router-view>
    </div>'
}

const router = new VueRouter({
    route:[
        {
            path:'/user/:id',
            component:User,
            children:[
                {
                    path:'profile',
                    component:UserProfile
                },
                {
                    path:'post',
                    component:UserPost
                }
            ]
        }
    ]
})
当/user/:id/prodfile 匹配成功,UserProfile会被渲染在User的<router-view>中
当/user/:id/post匹配成功,UserPost会被渲染在User的<router-view>中

/开头的嵌套路径会被当做根路径。这让你充分使用嵌套组件而无需设置嵌套的路径

children的配置就像是routes配置杨的路由配置数据。所以你可以嵌套多层路径

基于上面的配置当你访问/user/foo时,User的出口是不会渲染任何东西,这是因为没有匹配到合适的子路由 如果你想要渲染点什么,可以提供一个空的子路由

const router = new VueRouter({
    routes:[
        {
            path:'/user/:id',
            component:User,
            children:[
                {
                    path:'',
                    component:UserHome
                }
            ]
        }
    ]
})

编程式的导航

除了使用<router-link>创建a标签来定义导航链接,我们还可以借助router 的实例方法,来编写代码实现

  • router.push(location,onComplete?,onAbort?)

在Vue实例内部,你可以通过$router访问路由实例。因此可以调用this.$router.push

想要导航到不同的URL,可以使用router.push方法。 这个方法会向history栈添加一个新的记录。所以,用户点击后退时,则会回到之前URL

当你点击<router-link :to="...">时,这个方法会在内部调用。所以点击<router-link>等同于调用router.push()

//字符串
router.push('home')

//对象
router.push(path:'home')

// 命名路由
router.push({name:'user',params:{userId:'123'}})

//带查询参数变成`/register/plan=private
router.push({path:'register',query:{plan:'private'}})

如果提供了path,params会被忽略 同样的规则也试用于router-link组件的to属性

const userId = '213'

router.push({name:'user',params:{userId}}) // /user/123

router.push(path:`/user/${userId}`) // /user/123

router.push(path:'/user',params:{userID}) // /user
  • router.replace(location,onComplete?,onAbort?)

router.push很像,唯一不同就是,它不会想history添加新纪录,而是替换掉当前的history记录

声明式
<router-link :to="..." replace></router-link>
编程式
router.replace(...)
  • router.go()

这个方法参数是一个整数,意思是在history记录中向前或者向后多少步,类似window.history.go()

//在浏览器记录中前进一步
router.go(1)

// 后退一步
router.go(-1)

// 前进3步记录
router.go(3)

// 如果history记录不够用,就会是失败
router.go(100)
router.go(-100)

命名路由

创建Router实例的时候在routes配置中给某个路由设置名称

const router = new VueRouter({
    routes:[
        {
            path:'/user/:userId',
            name:'user',
            component:User
        }
    ]
})

<router-link to={name:'user',params:{userId:123}}>User</router-link>

router.push({name:'user',params:{userId:123}})

这两种方式都会把路由导航到`/user/123`路径

命名视图

有时候想同时同级展示多个视图,而不是嵌套视图 比如创建一个sidebar和main 两个视图,这时候命名视图就派上用场了 你可以在界面上拥有多个单独命名的视图,而不是只有一个单独的出口。 如果<router-view>没有设置名字,默认是default

<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>

一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用components配置

const router = new VueRouter({
    routes:[
        {
            path:'/',
            components:{
                default:Foo,
                a:Bar,
                b:Baz
            }
        }
    ]
})

嵌套命名视图

  • /settings/emials

    • UserSettings
      • Nav
      • UserEmailsSubsctipyions
  • /settings/profile

    • UserSettings
      • Nav
      • UserProfile
      • UserProfilePreview

Nav是一个常规组件 UserSettings是一个视图组件 UserEmailsSubsctiptions,userProfile,userProfilePreview 是嵌套组件

// userSettings
<div>
    <h1>User Settings</h1>
    <NavBar/>
    <router-view/>
    <router-view name="helper"/>
</div>

{
    path:'/settings',
    component:UserSettings,
    children:[
        {
            path:'emails',
            component:UserEmailsScriptions
        },
        {
            path:'profile',
            components:{
                default:UserProfile,
                helper:UserProfilePreview
            }
        }
    ]
}

重定向和别名

重定向

const router = new VueRouter({
    routes:[
        {
            path:'/a',
            redirect:'/b'
        }
    ]
})

也可以重定向到一个命名路由
const router = new VueRouter({
    routes:[
        {
            path:'/a',
            redirect:{
                name:'foo'
            }
        }
    ]
})

也可以是一个方法动态返回重定向目标

const router = new VurRouter({
    routes:[
        {
            name:'/a',
            redirect:to=>{
                // 方法接收目标路由作为参数,
                // return 重定向的字符串路径或路径对象
            }
        }
    ]
})

别名

重定向的就是当用户访问/a时,URL将会被替换成/b,然后匹配路由为/b

别名就是 /a的别名是/b,当用户访问/b时,URL会保持/b,但是路由则匹配/a,就象访问/a一样

const router = new VueRouter({
    routes:[
        {
            path:'/a',
            component:A,
            alias:'/b'
        }
    ]
})

别名的功能让你自由的将UI结构映射到任意URL,而不受限于配置的嵌套路由结构

路由组件传递参数

在组件中是$route会使之与其对应的路由形成高度耦合,从而使组件只能在某些特定的URL上使用,限制了灵活性

使用props将组建和路由进行解耦:

  • 取代与$route的耦合
const User = {
    template:'<div>User {{$route.params.id}}</div>'

}

const router = new VueRouter({
    routes:[
        {
            path:'/user/:id',
            component:User
        }
    ]
})

通过props解耦
const User = {
    props:['id'],
    template:'<div>User {{id}}</div>'
}

const router = new VueRouter({
    routes:[
        {
            path:'/user/:id',
            component:User,
            props:true
        },
        {
            path:'user/:id',
            components:{
                default:User,
                sidebar:Sidebar
            },
            props:{
                default:true,
                sidebar:true
            }
        }
    ]
})

布尔模式

如果props被设置为true,route.params将会被设置为组件属性

对象模式

如果props是一个对象,它会按原样设置为组件属性,当props是静态的时候有用。

const router = new VueRouter({
    routes:[
        {
            path:'/promotion/from-newsletter',
            component:Promotion,
            props:{
                newsletterPopup:false
            }
        }
    ]
})

函数模式

你可以创建一个函数返回props,这样你便可以将参数转换成另一种类型,静态值和基于路由的值结合等

const router = new VueRouter({
    routes:[
        {
            path:'/search',
            component:SearchUser,
            props:(route)=>{
                query:route.query.q
            }
        }
    ]
})

URL/search?q=vue会将{query:'vue'}作为属性传递searchUser组件

请尽可能保持props函数为无状态的,因为它只会在路由发生变化时起作用。

导航守卫

完整的导航解析流程

  1. 导航被触发
  2. 在失活的组件调用离开守卫
  3. 调用全局beforeEach守卫
  4. 在重用的组件里调用beforeRouteUpdate
  5. 在路由配置里调用beforeEnter
  6. 解析异步路由组件
  7. 在被激活的组件里调用beforeRouterEnter
  8. 调用全局的beforeResolve
  9. 导航被确认
  10. 调用全局的afterEach
  11. 触发DOM更新
  12. 用创建好的实例调用beforeRouterEnter守卫中传给next回调函数

全局前置守卫

router.beforeEach注册一个全局前置守卫:

const router = new VueRouter({...})
router.beforeEach((to,from,next)=>{//...})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有resolve完之前一直处于等待中

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

  • to:Route 即将要进去的目标路由对象

  • from:Route 当前导航正要离开的路由

  • next:Function 一定要调用该方法resolve这个钩子,执行效果依赖next方法调用参数

    • next():进行管道中的下一个钩子.吐过全部钩子执行完了,则导航状态就是confirmed
    • next(false):中断当前导航.如果浏览器URL变了,那么URL地址会重置到from路由对应的地址
    • next('/')/next({path:'/'}):跳转到一个不同的地址
    • next(error):如果传入一个next参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调

2确保要调用next方法。否则钩子就不会被resolve

全局解析守卫

router.beforeResolve注册一个全局守卫。 和router.beforeEach类似 区别实在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用

全局后置钩子

和守卫不用的是,这些钩子不会接受next函数也不会改变导航本身

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

路由独享的守卫

const router = new VueRouter({
    routes:[
        {
            path:'/foo',
            component:Foo,
            beforeEnter:(to,from,next)=>{
                //...
            }
        }
    ]
})

组件内的守卫

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave
<template>
//...
</template>

<script>
export default{
    beforeRouteEnter(ro,from,next)=>{
        //在渲染该组件的对应路由被confirm前调用
        // 不能获取组件实例`this`
        // 因为当守卫执行前,组件实例还没有创建

        //可以传一个回调给next来访问组件实例
        // 是支持给next传递回调的唯一守卫
        // next(vm=>{})

    },
    beforeRouteUpdate(to,from,next)=>{
        // 在当前路由改变,但是该组件被复用时调用
        // 可以访问组件实例`this`

    },
    beforeRouteLeave(to,from,next)=>{
        // 导航离开该组件的对应路由时调用
        // 可以访问组件实例`this`
        const answer=window.confirm('你还有未保存的信息,确定要离开吗?')
        if(answer){
            next()
        }else{
            next(false)
        }
    }
}
</script>

路由元信息

定义路由的时候可以配置meta字段

const router = new VueRouter({
    routes:[
        {
            path:'/foo',
            component:Foo,
            children:[
                {
                    path:'bar',
                    component:Bar,
                    meta:{
                        requiresAuth:true
                    }
                }
            ]
        }
    ]
})

我们称routes配置中的每个路由对象为路由记录。 路由记录是可以嵌套的。因此一个路由匹配成功后,它可能匹配多个路由记录 根据上面的路由配置,/foo/bar这个URL将会匹配父路由几率以及子路由记录

一个路由匹配到的所有路由记录会暴露在route对象的route.matched数组中。 因此我们需要遍历$route.matched来检查路由记录中的meta字段

router.beforeEach((to,from,next)=>{
    if(to.matched.some(record=>record.meta.requiresAuth)){
        if(!auth.loggedIn()){
            next({
                path:'/login',
                query:{redirect:to.fullPath}
            })
        }else{
            next()
        }
    }else{
        next()
    }
})

过渡动效

单个路由的过渡

<transition>
    <router-view></router-view>
</transition>
这种用法会给所有路由设置一样的过渡效果
如果想让每个路由组件有个子的过渡效果,可以在各路由组件内使用`<transition>`并设置不同的name

const Foo = {
    template:`<transition><div class="foo">...</div></transition>`
}
const Bar = {
    template:`<transition name="fade"><div></div></transition>`
}

基于路由的动态过渡

<transition :name="transitionName">
    <router-view></router-view>
</transition>

watch:{
    `$route`(to,from){
        const toDepth = to.path.split('/').length
        const fromDepth = from.path.split('/').length
        this.transitionName = toDepth < fromDepth ? 'slide-right': 'slide-left'
    }
}

数据获取

  • 导航完成之后获取:先完成导航,然后再接下来的组件生命周期钩子中获取数据,在数据获取期间显示loading提示
  • 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功之后执行导航

导航完成后获取数据

在导航完成前获取数据

我们可以在接下来的组件的beforeRouteEnter守卫中获取数据,当数据获取成功后只调用next方法

export default{
    data(){
        return {
            post:null,
            error:null
        }
    },
    beforeRouteEnter(to,from,next){
        getpost(to.params.id,(err,post)=>{
            next(vm=>vm.setData(err,post))
        })
    },
    beforeRouteUpdate(to,from,next){
        this.post = null;
        getPost(to.params.id,(err,post)=>{
            this.setData(err,post)
            next()
        })
    },
    methods:{
        setData(err,post){
            if(err){
                this.error = err.toString()
            }else{
                this.post = post
            }
        }
    }
}
在为后面的视图获取数据时,用户停留在当前的页面,
因此建议在数据获取期间,显示一些进度条或者别的提示,
获取失败,同样有必要展示一些全局的错误提示

滚动行为

路由懒加载