2

SPA单页应用

传统的项目大多使用多页面结构,需要切换内容的时候我们往往会进行单个html文件的跳转,这个时候受网络、性能影响,浏览器会出现不定时间的空白界面,用户体验不好。现在的应用都流行SPA应用(single page application),单页面应用就是用户通过某些操作更改地址栏url之后,动态的进行不同模板内容的无刷新切换,用户体验好。

Vue中会使用官方提供的vue-router插件来使用单页面,原理就是通过检测地址栏变化后将对应的路由组件进行切换(卸载和安装)。

简单路由的实现

  1. 如果不是使用脚手架创建的项目,需要手动安装vue-router路由模块。

     cnpm install vue-router -S
  2. 引入vue-router,如果是在脚手架中,引入VueRouter之后,需要通过Vue.use来注册插件。

    import Vue from 'vue'
    import Router from 'vue-router'
    Vue.use(Router)//让vue可以使用vue-router插件

    注册vue-router插件之后,this上就有了$route/router属性。

image

  1. 创建router路由器,并导出。

    let router = new Router({
      routes:[
    
      ]
    })
    
    export default router;
  2. 在根实例里注入router

    new Vue({
      //挂载router
      router,//为了可以让组件使用this.$route,this.$router的一些api属性或者方法
      store,
      render: h => h(App)
    }).$mount('#app')
    

    此时组件可以通过this.$router/router来使用路由相关功能的api。

    image

  3. 创建router路由表

    let router = new Router({
      routes:[
        {
          path:"/home",component:Home
        },
        {
          path: "/list", component: List
        },
        {
          path: "/mine", component: Mine
        }
      ]
    })
  4. 在App.vue中利用router-view来指定路由切换的位置

    <template>
      <div id="app">
        Hello app.vue!
        <!-- 显示路由组件的位置 -->
        <router-view></router-view>
      </div>
    </template>

    路由切换效果:

    image

  5. 使用router-link来创建切换路由的工具

    <div>
          <router-link to = "/home">首页</router-link>
          <router-link to = "/list">列表</router-link>
          <router-link to = "/mine">我的</router-link>
        </div>

    image

    <router-link>会渲染成a标签,添加to属性来设置要更改的path信息

    image

路由的懒加载

懒加载也叫延迟加载,即在需要的时候进行加载,随用随载。在单页应用中,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,延时过长,会出现长时间的白屏,即使做了loading也是不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时。简单的说就是:进入首页时不用一次加载过多资源,造成页面加载用时过长。

懒加载写法:

// 路由的懒加载方式
    { path :"/home",component:()=>import("../views/Home")},// 当我访问/home首页时,页面才去加载Home组件,减少首页加载的时长
    { path :"/list",component:()=>import("../views/List")},
    { path :"/mine",component:()=>import("../views/Mine")}

非按需加载则会把所有的路由组件块的js包打在一起。当业务包很大的时候建议用路由的按需加载(懒加载)。 按需加载会在页面第一次请求的时候,把相关路由组件块的js添加上。

二级路由(路由嵌套)

在创建路由表的时候,可以为每一个路由对象创建children属性,值为数组,在这个里面又可以配置一些路由对象来使用多级路由,注意:一级路由path前加'/',二级路由前不需要加'/'。

{ path :"/list",component:()=>import("../views/List"),children:[
       // 二级路由前不需要加“/”
      { path: "audio", component: () => import("../views/Audio") },
      { path: "video", component: () => import("../views/Video") }
    ]}
二级路由组件的切换位置依然由router-view来指定(指定在父级路由组件的模板中)。

在List.vue中通过<router-view>来确定二级路由组件的位置。

<div>
      <router-link to = "/list/audio">音频</router-link>
      <router-link to = "/list/video">视频</router-link>
    </div>
    <router-view></router-view> 

二级路由切换效果:

image

router-link

<router-link> 组件支持用户在具有路由功能的应用中(点击)导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 标签,可以通过配置 tag 属性生成别的标签。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。

to

router-link的to属性,默认写的是path(路由的路径),可以通过设置一个对象,来匹配更多。

<router-link tag = "li" :to = "{name:'detail',params:{id:'1'},query:{title:'最近播放'}}">我的歌单</router-link>

name

name是要跳转的路由的名字,也可以写path来指定路径,但是用path的时候就不能使用params传参,params是传路由参数,query传queryString参数。

replace

路由跳转到不同的url默认是push的过程,当用户点击浏览器后退按钮式时,则回到之前url,replace属性可以控制router-link的跳转不被记录。

依次点击首页——列表——音频——视频——我的,点击返回按钮时,依次返回之前的url。

image

在List.vue的<router-link>标签中添加replace属性,则该标签内的跳转不会被记录

<router-link
        v-for = "nav in navs"
        :key = "nav.id"
        :to = "{name:nav.name}"
        active-class = "title"
        replace
      >
        {{nav.title}}
      </router-link>

再依次点击首页——列表——音频——视频——我的,点击返回按钮时,可以看到从音频——视频之间的跳转没有被记录。

image

active-class

<router-link>且会根据当前路由的变化为a标签添加对应的router-link-active/router-link-exact-active(完全匹配成功)类名,我们可以通过它来为标签设置不同的选中样式。

<style lang="scss">
.router-link-active{
    color:blue;
  }
  .router-link-exact-active{
    color:red;
    font-weight:900;
  }
</style>

标签切换时,选中状态的显示效果:

image

还可以通过<router-link>的active-class属性给a标签添加指定一个activeClass名,通过设置这个class的样式,来设置标签选中时的状态,功能类似于router-link-exact-active。

<router-link
        v-for = "nav in navs"
        :key = "nav.id"
        :to = "nav.path"
        active-class = "title"
      >
        {{nav.title}}
      </router-link>

设置.title样式

<style lang = "scss" scoped>
  .title{
    color:aquamarine;
  }
</style>

显示效果:

image

tag

<router-link>默认渲染成带有正确链接的a标签,可以通过配置 tag 属性生成别的标签。

<ul>
      <router-link tag = "li">我的歌单</router-link>
      <router-link tag = "li">最近播放</router-link>
    </ul>

image

参数传递

有的时候我们需要在路由跳转的时候跟上参数,路由传参的参数主要有两种:路由参数、queryString参数。

路由参数

在router路由表中配置动态路由,下面的代码就是给detail路由配置接收id的参数,多个参数继续在后面设置。

// 配置动态路由
    { path:"/detail/:id",component:()=>import("../views/Detail.vue")}

在页面中通过<router-link>的to属性传递参数:

<ul>
      <router-link tag = "li" to = "/detail/1">我的歌单</router-link>
      <router-link tag = "li" to = "/detail/2">最近播放</router-link>
    </ul>

在Detail.vue中打印this.$route.params.id

export default {
    created(){
        //获取动态路由传递过来的参数
        console.log(this.$route.params.id)
    }
}

在页面中通过$route.params.id渲染获取到的动态id

<template>
    <div class = "detail">
        我是详情页,我的动态id是:{{$route.params.id}}
    </div>
</template>

image

queryString参数

queryString参数不需要在路由表设置接收,直接设置?后面的内容:

<ul>
      <router-link tag = "li" to = "/detail/1?title=我的歌单">我的歌单</router-link>
      <router-link tag = "li" to = "/detail/2?title=最近播放">最近播放</router-link>
    </ul>

在路由组件中通过this.$route.query接收

export default {
    created(){
        //获取动态路由传递过来的参数
        console.log(this.$route.params.id,this.$route.query.title)
    }
}

打印结果:

image

页面渲染效果:

image

上面的参数传递也可以写成对象的形式:

<!-- 写成对象的形式 -->
      <router-link tag = "li" :to = "{path:'/detail/2',query:{title:'最近播放'}}">最近播放</router-link>

通过prop将路由与组件解耦

在组件中接收路由参数需要this.$route.params.id,代码冗余,现在可以在路由表里配置props:true。

{ path:"/detail/:id",component:()=>import("../views/Detail.vue"),name:"detail",props:true}

在路由组件中可以通过props接收id参数去使用

props:["id"],

在页面中就可以通过{{id}}的方式来使用路由传递的参数

我的动态id是:{{id}}

默认路由和重定向

默认路由

当我们进入应用,默认想显示某一个路由组件,或者当我们进入某一级路由组件的时候想默认显示其某一个子路由组件,我们可以配置默认路由。

设置当用户访问"/list"的时候默认显示"/list/audio"界面

{ path :"/list",component:()=>import("../views/List"),children:[
       // 二级路由前不需要加“/”
      { path :"",redirect:"audio"},
      { path: "audio", component: () => import("../views/Audio") },
      { path: "video", component: () => import("../views/Video") }
    ]}

image

重定向

有时候虽然当前url与路由表不匹配,但是我们希望跳转到同一个页面或者说是打开同一个组件,这时候我们就需要使用redirect参数重定向到其他路由。

如果我们直接写成如下形式:

{ path: "/", component: () => import("../views/Home")},

虽然当我们访问"/"的时候可以显示"/home"页面,但是可以看到标签上并没有添加router-link-exact-active的class名,所以无法显示选中的样式。

image

我们需要写成路由重定向形式:

//路由重定向
    { path :'/',redirect:"/home"}

当我们在地址栏访问"/"的时候会自动跳转到"/home"页面,并且标签可以显示选中样式。

image

命名路由

我们可以给路由对象配置name属性,这样的话,我们在跳转的时候直接写"name:名称"就会快速的找到此name属性对应的路由,不需要写大量的urlpath路径了。

将之前配置的路由添加name属性:

{ path: "audio", component: () => import("../views/Audio"),name:"audio"},
{ path: "video", component: () => import("../views/Video"),name: "video"}

在List.vue的data中将path改为name属性:

data(){
    return{
      navs:[
        /* {id:1,title:"音频",path:"/list/audio"},
        {id:2,title:"视频",path:"/list/video"} */
        {id:1,title:"音频",name:"audio"},
        {id:2,title:"视频",name:"video"}
      ]
    }
  }

将<router-link>中的to属性改为"name:名称"形式

<router-link
        v-for = "nav in navs"
        :key = "nav.id"
        :to = "{name:nav.name}"
        active-class = "title"
      >
        {{nav.title}}
      </router-link>

可以看到实现效果是完全相同的:

image

给detail也添加一个name属性

{ path:"/detail/:id",component:()=>import("../views/Detail.vue"),name:"detail"}

Home.vue中的<router-link>可以改写成如下格式:

<router-link tag = "li" :to = "{name:'detail',params:{id:'1'},query:{title:'最近播放'}}">我的歌单</router-link>

params传递的参数通过this.$route.parmas.id来获取

query传递的参数通过this.$route.query.title来获取

编程式导航

<router-link>可以让我们直接跳转页面,但是有的时候需要在跳转前进行一些动作,这时候我们可以方法里使用$router的方法。

当我们想在Mine我的页面添加一个点击返回首页的链接时,可以通过<router-link>标签来实现:

<div class="home">
    <router-link to = "/">mine我的</router-link>
  </div>

image

也可以通过编程式导航来实现,在页面中添加一个button按钮,给按钮添加一个点击事件clickMe

<button @click = "clickMe">点击返回首页</button>

在method中编写clickMe方法,通过$router的push方法,让它跳转到"/"页面

methods:{
    clickMe(){
      let flag = confirm("是否返回首页")
      if(flag){
        // 编程式导航
        this.$router.push("/");
      }
    }
  }

image

路由模式

为了构建SPA(单页面应用),需要引入前端路由系统,这也就是Vue-router存在的意义。前端路由的核心,就在于:改变视图的同时不会向后端发出请求。

hash和history这两个方法应用于浏览器的历史记录站,在当前已有的back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改是,虽然改变了当前的URL,但你浏览器不会立即向后端发送请求。

hash:即地址栏URL中的#符号(此hsah 不是密码学里的散列运算)

路由有两种模式:hash、history,默认会使用hash模式

image

比如这个URL:http://www.baidu.com/#/hello, hash 的值为#/hello。它的特点在于:hash 虽然出现URL中,但不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。

history:利用了HTML5 History Interface 中新增的pushState() 和replaceState() 方法。(需要特定浏览器支持)

如果url里不想出现丑陋hash值(#),在new VueRouter的时候配置mode值为history来改变路由模式,本质使用H5的histroy.pushState方法来更改url,不会引起刷新。

// 默认会使用hash模式
  mode:"history",

image

history模式,会出现404 的情况,需要后台配置。

404错误

  1. hash模式下,仅hash符号之前的内容会被包含在请求中,当用户访问https://www.baidu.com/#/home...:https://www.baidu.com/。因此对于后端来说,即使没有做到对路由的全覆盖,页面也不会返回404错误;image
  2. history模式下,前端的url必须和实际向后端发起请求的url 一致,因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 https://www.baidu.com/home/de...,缺少对/home/detail的路由处理,就会返回 404错误,这就不好看了。

    image

所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

导航守卫(路由钩子/路由守卫/路由生命周期)

正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的,单个路由独享的,,或者组件级的。

在某些情况下,当路由跳转前或跳转后、进入、离开某一个路由前、后,需要做某些操作,就可以使用路由钩子来监听路由的变化。

全局守卫

全局前置守卫beforeEach

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

image

一定要在路由守卫中调用next()方法来resolve这个钩子

// 全局前置路由
router.beforeEach((to,from,next)=>{
  console.log("beforeEach:全局前置守卫")
  // 一定要调用next()方法来resolve这个钩子
  next();
})

image

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

  • to: Route: 即将要进入的目标路由对象
  • from: Route: 当前导航正要离开的路由

    // 全局前置路由
    router.beforeEach((to,from,next)=>{
      console.log("beforeEach:全局前置守卫")
      if(to.path === "/mine") {
        alert("即将进入mine我的页面")
      }
      if(from.path === "/home"){
        alert("从首页离开")
      }
      // 一定要调用next()方法来resolve这个钩子
      next();
    })

    image

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

    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: truename: 'home' 之类的选项以及任何用在 router-linkto prop或 router.push 中的选项。
    • next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError()注册过的回调。

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

全局后置钩子afterEach

也可以注册全局后置钩子,然而和前置守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

// 全局后置钩子
router.afterEach((to,from)=>{
  if (to.path === "/list/audio") {
    alert("已经进入list列表audio页面")
  }
})

image

局部守卫

路由独享的守卫

在路由配置上直接定义beforeEnter守卫:

{ path :"/mine",component:()=>import("../views/Mine"),beforeEnter(to,from,next){
      console.log("进入到mine页面了")
      next();
    }}

image

组件内的守卫

最后,你可以在路由组件内直接定义以下路由导航守卫:

  1. beforeRouteEnter

    在进入该组件之前执行,该路由守卫中获取不到实例this,因为此时组件实例还没被创建。beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

    在Home.vue中添加beforeRouteEnter:

    beforeRouteEnter(to,from,next){
        console.log("beforeRouteEnter:进入组件之前",this)
        next()
      }

    打印this,显示undefined

    image

    不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

    beforeRouteEnter (to, from, next) {
      next(vm => {
        // 通过 `vm` 访问组件实例
      })
    }

    注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdatebeforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。

  2. beforeRouteLeave

    在离开该组件之前执行,该路由守卫中可以访问组件实例"this"。

    在Home.vue中添加beforeRouteLeave:

      beforeRouteLeave(to,from,next){
        console.log("beforeRouteLeave:离开组件之前",this)
        next()
      }

    此时可以打印组件实例"this"

    image

    这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。

  3. beforeRouteUpdate(2.2 新增)

    当一个组件被重复调用的时候执行该守卫

    在Deatil.vue中添加beforeRouteUpdate:

        beforeRouteUpdate(to,from,next){
            console.log("beforeRouteUpdate")
            next()
        }

    image


视觉派Pie
196 声望27 粉丝

Web前端程序猿