Vue Router学习笔记

Vue Router

1、SPA(Single Page Application)

  • 后端渲染(存在性能问题)
  • Ajax前端渲染(前端渲染提高性能,但是不支持浏览器的前进后退操作)
  • SPA单页应用程序:整个网站只有一个页面,内容的变化通过Ajax局部更新实现、同时支持浏览器地址的前进和后退。
  • SPA实现原理之一:基于URL地址的hash(hash变化会导致浏览器记录访问历史的变化,但是hash的变化不会触发新的URL请求)
  • 在实现SPA的过程中,最核心的技术点就是前端路由。

2 Vue Router的概念

VueRouter 是 SPA(单页应用)的路径管理器,它允许我们通过不同的 URL 访问不同的内容。

先了解 VueRouter 的两个内置组件:

<router-link>:该组件用于设置一个导航链接,切换不同 HTML 内容。 to 属性为目标地址,即要显示的内容。例:<router-link to="/index">首页</router-link><router-link>默认会被渲染为 a 标签,to 属性默认会被渲染为 href 属,性,to 属性的值默认会被渲染为 # 开头的 hash 地址。

<router-view>:通过路由规则匹配到的组件,将会被渲染到 <router-view> 所在的位置。

2.1 使用步骤

  1. 引入相关的库文件
<!-- 导入 vue 文件,为全局 window 对象挂载 Vue 构造函数 --> 
<script src="./lib/vue_2.5.22.js"></script>
<!-- 导入 vue-router 文件,为全局 window 对象挂载 VueRouter 构造函数 -->
<script src="./lib/vue-router_3.0.2.js"></script>
  1. 添加路由链接
<router-link to='/user'>User</router-link>
<router-link to='/register'>Register</router-link>
  1. 添加路由填充位
<router-view></router-view>
  1. 定义路由组件
   let User = {
        template:`<div>User组件</div>`
    }
    let Register = {
        template:`<div>Register组件</div>`
    }
  1. 配置路由规则并创建路由实例
    const routes = [
        {path:'/user',component:User},
        {path:'/register',component:Register}
    ]
    var router = new VueRouter({
        routes
    })
  1. 把路由挂载到 Vue 根实例中
    const vue = new Vue({
        el:'#app',
        router
    })

2.2 嵌套路由

2.2.1 基本使用

实际项目中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件。

要配置嵌套路由,我们需要在配置的参数中使用children 属性。

  {
    path: '路由地址',
    component: '渲染组件',
    children: [
      {
        path: '路由地址',
        component: '渲染组件'
      }
    ]
  }

  1. 嵌套路由功能分析
    • 点击父级路由链接显示模板内容
    • 模板内容中又有子级路由链接
    • 点击子级路由链接显示子级模板内容
      在这里插入图片描述
  • 父路由组件模板
    父级路由链接
    父组件路由填充位
    <div id="app">
        <p>
            <router-link to='/user'>User</router-link>
            <router-link to='/register'>Register</router-link>
        </p>
        <router-view></router-view>
    </div>
  • 子级路由模板
const Register = {
template: `<div>
<h1>Register 组件</h1>
<hr/>
<router-link to="/register/tab1">Tab1</router-link>
<router-link to="/register/tab2">Tab2</router-link>
<!-- 子路由填充位置 --> <router-view/>
</div>`
}
  • 嵌套路由配置
const router = new VueRouter({
routes: [
{ path: '/user', component: User },
{
path: '/register',
component: Register,
// 通过 children 属性,为 /register 添加子路由规则
children: [
{ path: '/register/tab1', component: Tab1 },
{ path: '/register/tab2', component: Tab2 }
] } ]
})

完整示例

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>

</style>

<body>
    <div id="app">
        <!-- router-link 是 vue 中提供的标签,默认会被渲染为 a 标签 -->
        <!-- to 属性默认会被渲染为 href 属性 -->
        <!-- to 属性的值默认会被渲染为 # 开头的 hash 地址 -->
        <router-link to="/user">User</router-link>
        <router-link to="/register">Register</router-link>

        <!-- 路由填充位(也叫做路由占位符) -->
        <!-- 将来通过路由规则匹配到的组件,将会被渲染到 router-view 所在的位置 -->
        <router-view></router-view>
    </div>
</body>

<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript" src="js/vue-router_3.0.2.js"></script>
<script type="text/javascript">
    const User = {
        template: '<h3>User</h3>'
    }
    const Register = {
        template: `<div>
        <h3> Register组件</h3>
        <hr>
        // 子路由链接
        <router-link to="/register/Tab1">Tab1</router-link>
        <router-link to="/register/Tab2">Tab2</router-link>
        // 子路由占位符
        <router-view></router-view>
        </div>`
    }

    const Tab1 = {
        template: '<h3>Tab1</h3>'
    }

    const Tab2 = {
        template: '<h3>Tab2</h3>'
    }

    // 创建路由实例对象
    const router = new VueRouter({
        // routes 是路由规则数组
        routes: [
            // 每个路由规则都是一个配置对象,其中至少包含 path 和 component 两个属性:
            // path 表示当前路由规则匹配的 hash 地址
            // component 表示当前路由规则对应要展示的组件
            {
                path: '/',
                redirect: '/user'
            }, {
                path: '/user',
                component: User
            }, {
                path: '/register',
                component: Register,
                children: [{
                    path: '/register/Tab1',
                    component: Tab1
                }, {
                    path: '/register/Tab2',
                    component: Tab2
                }]
            }
        ]
    })

    const vue = new Vue({
        el: '#app',
        // router: router,
        router, //es6中属性名和属性值一样可以简写。
        methods: {

        },
    })
</script>
</html>

2.3 定义路由地址

在上述的例子中,我们通过 ‘/register/tab1’ 来访问嵌套路由,但是有时候你可能不希望使用嵌套路径,这时候我们可以对上面例子中的配置信息做一点修改:

const routes = [
        {path:'/user',component:User},
        {path:'/register',
        component:Register,
        children:[
            {path:'/registerTab1',component:Tab1},
            {path:'/registerTab2',component:Tab2}
        ]
    
        }
    ]

2.4 路由命名

我们可以在 route 对象中添加一个 name 属性用来给路由指定一个名字:

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

2. 5 vue-route编程式导航

2.5.1 页面导航的两种方式

  • 声明式导航:通过点击链接实现导航的方式,叫做声明式导航
    例如:普通网页中的 链接 或 vue 中的
  • 编程式导航:通过调用JavaScript形式的API实现导航的方式,叫做编程式导航
    例如:普通网页中的 location.href

Vue Router 提供了router的实例方法,通过编写代码来实现导航功能。在 Vue 实例内部,你可以通过$router访问路由实例。

2.5.2 router.push

这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。

基本用法
  <div id="app">
    <div>
      <button @click="jump('index')">首页</button>
      <button @click="jump('article')">文章</button>
    </div>
    <router-view></router-view>
  </div>
 var vm = new Vue({
    el: '#app',
    router,
    data() {
    	return {}
    },
    methods: {
      jump(name) {
        this.$router.push(name)
      }
    }
  })
对象格式的参数

// 字符串形式的参数
router.push('home')

// 通过路径描述地址
router.push({ path: 'home' })

// 通过命名的路由描述地址
router.push({ name: 'user' }})

当以对象形式传递参数的时候,还可以有一些其他属性,例如查询参数 params、query。

2.5.3 router.replace

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

<div id="app">
    <div>
      <button @click="jump('index')">首页</button>
      <button @click="jump('article')">文章</button>
    </div>
    <router-view></router-view>
  </div>
  var vm = new Vue({
    el: '#app',
    router,
    data() {
    	return {}
    },
    methods: {
      jump(name) {
        this.$router.replace(name)
      }
    }
  })

2.5.4 router.go

这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步。


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

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

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

// 如果 history 记录不够用,路由将不会进行跳转
router.go(-100)
router.go(100)
<div id="app">
    <div>
      <router-link to="index">首页</router-link>
      <router-link to="article">文章</router-link>
    </div>
    <button @click="go(1)">前进一步</button>
    <button @click="go(-1)">后路一步</button>
    <button @click="go(3)">前进三步</button>
    <button @click="go(-3)">后路三步</button>
    <router-view></router-view>
  </div>
 var vm = new Vue({
    el: '#app',
    router,
    data() {
    	return {}
    },
    methods: {
      go(n) {
        this.$router.go(n)
      }
    }
  })

2.5.5 <router-link> 跳转命名路由

实际上 router-link 的 to 属性可以接收一个对象。

 <router-link :to="{path: '/index'}">首页</router-link>
      <router-link to="/article">文章</router-link>

除了通过 path 可以链接到路由外,还可以通过路由 name 实现链接跳转:

<div id="app">
    <div>
      <router-link :to="{name: 'index'}">首页</router-link>
      <router-link :to="{name: 'article'}">文章</router-link>
    </div>
    <router-view></router-view>
  </div>

3.5.6 编程式导航跳转命名路由

<div id="app">
    <div>
      <button @click="jump('index')">首页</button>
      <button @click="jump('article')">文章</button>
    </div>
    <router-view></router-view>

const routes = [
  { path: '/index', name: 'index', component: Index },
  { path: '/article', name: 'article' , component: Article }
]

const router = new VueRouter({
  routes: routes
})

  var vm = new Vue({
    el: '#app',
    router,
    data() {
    	return {}
    },
    methods: {
      jump(name) {
        this.$router.push({
          name: name
        })
      }
    }
  })

2.6 路由重定向和路由别名

路由别名和重定向在项目中经常使用。

2.6.1 路由重定向

重定向也是通过 routes 配置来完成,可以配置路由重定向到具体路由地址、具名路由或者动态返回重定向目标。

重定向到路由地址

路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面;
通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:

var router = new VueRouter({
routes: [
	// 其中,path 表示需要被重定向的原地址,redirect 表示将要被重定向到的新地址
	{path:'/', redirect: '/user'},
	{path:'/user',component: User},
	{path:'/register',component: Register}
	]
})
重定向到具名路由

通过属性 redirect 重定向到具名路由:

    const routes = [
        {path:'/',redirect:{name:'user'}},
        {path:'/user',component:User,name:'user'},
        {path:'/register',
        component:Register,
        children:[
            {path:'/registerTab1',component:Tab1},
                {path:'/registerTab2',component:Tab2}
        ]
    
        }
    ]
动态返回重定向目标

属性 redirect 可以接收一个方法,动态返回重定向目标:

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

2.6.2 路由别名

当用户访问 /a 时,URL 将会被替换成 /b,然后匹配路由为 /b。
/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。

 <div id="app">
    <div>
      <router-link to="/index">首页</router-link>
      <router-link to="/article">文章</router-link>
    </div>
    <router-view></router-view>
  </div>
const routes = [
  { path: '/index', component: Index, alias: '/' },
  { path: '/article', component: Article }
]

const router = new VueRouter({
  routes: routes
})

var vm = new Vue({
  el: '#app',
  router: router,
  data() {
    return {}
  }
})

2.7 VueRouter 路由传参

通常我们有两种方式来传递参数:
(1) 在 Vue Router 中用query传递参数,query是拼接在url后面的参数,不是路由的一部分,比如/page/detail?id=123
(2)在 Vue Router 中用params传递参数,params要拼接在url里面,params是路由的一个部分,比如/page/detail/123。如果这个路由有params参数,但是在跳转的时候没有传递这个参数,会导致跳转失败或者页面没有内容。

2.7.1 params 传参

params方法传参的时候,要在路由后面加参数名占位;

路由配置
const router = new VueRouter({
  routes: [
    {
      path: "/home",
      component: Home,
      name: "Home",
      children: [
        // 动态路径参数以冒号 ":" 开头
        { path: "page1/:id", component: Page1, name: "Page1" }
      ]
    }
  ]
});
跳转方法
// 方法一:强制刷新参数会丢失
this.$router.push({ name: "Page1", params: { id: 123 } });
// 会跳转到 /home/page1/123

//方法二:强制刷新参数不会丢失
this.$router.push('/home/page/'+id)

//方法三: 强制刷新参数会丢失
<router-link :to="{ name: 'Page1', params: {id: 1234}}">goto Page1</router-link>
<!-- 点击会跳转到 /home/page1/1234 -->
获取参数的方式
<!-- Page1.vue -->
<template>
  <!-- $route 可直接注入到模板 -->
  <div>{{ $route.params.id }}</div>
</template>

2.7.2 query传参

query 的传参模式,不需要修改路由配置,只需要在导航的时候传参:

配置路由
const router = new VueRouter({
  routes: [
    {
      path: "/user",
      component: User,
      name: "users",
    }
  ]
});
跳转方法
// 方法一:
this.$router.push({ name: "users", query: { id: 123 } });
// 会跳转到 /user?id=123
// 方法二:
this.$router.push({path:'/user',query:{id:123}})
//方法三:
this.$router.push('/user?id='+id)
//方法四:
<router-link :to="{ name: 'users', query: {id: 1234}}">goto Page2</router-link>
//方法五:
<router-link :to="{ name: '/user', query: {id: 1234}}">goto Page2</router-link>
<!-- 点击会跳转到 /user?id=1234 -->
参数的使用
<!-- Page2.vue -->
<template>
  <!-- $route 可直接注入到模板 -->
  <div>{{ $route.query.id }}</div>
</template>
传递的参数是对象或者数组

如果query方式传递的是对象或者数组,在地址栏中会被强制转换成[object Object],刷新后页面获取不到对象。
我们需要通过JSON.stringify()方法将参数转换为字符串,在获取参数时通过JSON.parse转换成对象。

let parObj = JSON.stringify(obj)
// 路由跳转
this.$router.push({
   path:'/detail',
   query:{
       obj:parObj
   }
})

// 详情页获取参数
JSON.parse(this.$route.query.obj)

注意:这样虽然可以传对象,但是如果数据多的话地址栏会很长(不太推荐)。

使用props配合组件路由解耦

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

// 路由配置
{
   path:'/detail/:id',
   name:'detail',
   component:Detail,
   props:true             // 如果props设置为true,$route.params将被设置为组件属性  
}

// 路由跳转
this.$router.push({
   path:`/detail/${id}`
})

// 详情页获取参数
export default {
  props:['id'],      // 将路由中传递的参数id解耦到组件的props属性上
  mounted(){
    console.log("id",this.id);  
  }
}
// 路由配置
{
   path:'/detail',
   name:'detail',
   component:Detail,
   props:true             // 如果props设置为true,$route.params将被设置为组件属性  
}

// 路由跳转
this.$router.push({
   name:'detail',
   params:{
       order:{
         id:'123456789',
         name:'商品名称'  
       }
   }
})

// 详情页获取参数
export default {
  props:['order'],      // 将路由中传递的参数order解耦到组件的props属性上
  mounted(){
    console.log("order",this.order);  
  }
}

此外,数据量比较大的参数,可以使用sessionStorage或localStorage来进行存储参数来解决页面刷新参数丢失的问题,具体结合实际项目即可。

2.8 监听路由

2.8.1 方法一watch监听

这个时候,我们可以通过watch(监测变化)$route对象,来对路由参数的变化作出响应:

<template>
  <div>
    <div>Detail</div>
    <div>{{$route.query.id ? '修改' : '新建'}}</div>
    <div>name: <input v-model="detail.name" /></div>
    <div>text: <input v-model="detail.text" /></div>
  </div>
</template>

<script>
  // 下面是 Vue 组件
  export default {
    data() {
      return {
        detail: {
          name: "",
          text: ""
        }
      };
    },
    watch: {
      $route(to, from) {
        // 对路由变化作出响应,更新参数
        this.updateDetail();
      }
    },
    methods: {
      updateDetail() {
        const id = this.$route.query.id;
        if (id) {
          // 传入 id 则意味着修改,需要获取并录入原先内容
          this.detail = {
            name: `name-${id}`,
            text: `text-${id}`
          };
        } else {
          // 未传入 id 则意味着新建,需要重置原有内容
          this.detail = {
            name: "",
            text: ""
          };
        }
      }
    }
  };
</script>

我们可以通过 watch $route,在每次路由更新之后,重新获取 id 然后更新对应的内容。但上面这种做法依然存在问题,如果我们的路由从detail?id=123变成了detail?id=123&test=hahaha$route会出发侦听器,但是我们的 id 其实并没有变更,而这个时候由于重新获取内容,会覆盖掉我们正在编辑的内容。为了避免这种情况,我们可以通过参数(to, from)来检测是否不一致:

export default {
  watch: {
    $route(to, from) {
      // 对路由变化作出响应
      // 只有 id 值变更的时候,才进行更新
      if (to.query.id !== from.query.id) {
        this.updateDetail();
      }
    }
  }
};

2.8.2 方法组件内守卫beforeRouteUpdate

在当前路由改变时,并且该组件被复用时调用。

  • 对于一个带有动态参数的路径/foo/:id,在/foo/1和foo/2之间跳转的时候,组件实例会被复用,该守卫会被调用
  • 当前路由query变更时,该守卫会被调用。
const User = {
  template: '...',
  beforeRouteUpdate (to, from, next) {
    // react to route changes...
    // don't forget to call next()
  }
}

2.9 导航守卫

导航守卫就是路由跳转过程中的一些钩子函数,

全局路由一共分为三类:全局守卫、路由独享的守卫、组件内的守卫。

2.9.1 全局守卫

所谓全局守卫可以理解为只要触发路由跳转就会触发这些钩子函数。全局守卫的钩子函数的执行顺序:beforeEach(全局前置守卫)、beforeResolve(全局解析守卫)、afterEach(全局后置守卫)。

router.beforeEach(全局前置守卫)

在路由跳转前触发,参数包括to,from,next,这个钩子函数主要用于登录验证。
回调函数中的三个参数: to进入到哪个路由、from:从哪个路由离开,next函数决定是否展示你要看到的路由页面。

在main.js中,有一个路由实例化对象router,在main.js中设置守卫是全局守卫。

router.beforeEach((to, from, next) => {   //全局全局前置守卫
  if(to.name != 'login'){ //如果不是登录页面
    if(ISLOGIN)next()   //已登录就执行跳转
    else next({name:'login'})   //否则跳转到登录页面
  }else{ //如果是登录页面
    if(ISLOGIN)next({name:'/'}) //已登录就跳转到首页
    else  next()  //否则正常进入登录页面
  }
})
守卫方法的三个参数

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

  • to: Route: 即将要进入的目标 路由对象
  • from:Route:当前导航正要离开的路由
  • next:Function:决定是否展示你要看到的路由页面。
    (1)next():进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed(确认的)。
    (2)next(false):中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址hi重置到from路由对应的地址。
    (3)next('/')或者next({path:'/'}):跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next传递任意位置对象,且允许设置诸如replace:truename:'home'之类的选项以及任何用在router-linkto prop或者router.push中的选项
    (4)next(error):(2.4.0+)如果传入next的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调。
    **确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。**这里有一个在用户未能验证身份时重定向到 /login 的示例:
// BAD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  // 如果用户未能验证身份,则 `next` 会被调用两次
  next()
})
// GOOD
router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
  else next()
})
router.beforeResolve(全局解析守卫)

路由跳转前触发,参数也是to,from,next三个,它与beforeEach的却别在于:在跳转被确认之前,同时在所有组件内守卫和异步路由组件都被解析之后,解析守卫才调用。也就是在beforeEach和beforeEnterRouter之后,afterEach之前调用。

router.afterEach(全局后置钩子)

它是在路由跳转完成后触发,参数包括to和from,没有next参数。它发生在beforeEach和beforeResolve之后。

//设置路由跳转后,返回页面顶部
vueRouter.afterEach((to,from,next) => {
  window.scrollTo(0,0);
});

2.9.2 路由独享守卫

独享守卫只有一种:beforeEnter。永和全局守卫一致,只是要将独享路由守卫写进一个路由对象中。

如果都设置则在beforeEach之后紧随执行,参数to、from、next。
该守卫只在其他路由跳转至配置有beforeEnter路由表信息时才生效。
router配置文件内容:

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

2.9.3 组件内守卫

组件内守卫是跳转到这个组件时要执行的钩子函数,执行顺序分别为breforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。
三者分别对应:进入该路由时执行,该路由中参数改变时执行,离开该路由时执行。

beforeRouteEnter

路由进入之前调用,参数包括to、from、next。该钩子函数在全局前置守卫(beforeEach)和全局独享守卫beforeEnter之后,全局解析守卫beforeResolve和全局防守守卫afterEach之前调用。
注意在该钩子函数内获取不到组件的实例,也就是访问this为undefined,也就是它在beforeCreate声明周期函数之前触发。
**在这个钩子函数中,可以通过传一个回调next来访问组件实例。**在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数,可以在这个守卫中请求服务端获取数据,当成功获取并能介入路由时,调用next并在回调中通过vm访问组件实例并进行赋值等操作(next函数的调用发再生mounted之后:为了确保能对组件实例的完整访问。)

 beforeRouteEnter (to, from, next) {
  // 这里还无法访问到组件实例,this === undefined
  next( vm => {
    // 通过 `vm` 访问组件实例
  })
}
beforeRouteUpdate

在当前路由改变时,并且该组件被复用时调用,可以通过this访问实例,参数包括to、from、next。

  • 对于一个带有动态参数的路径/foo/:id,在/foo/1和foo/2之间跳转的时候,组件实例会被复用,该守卫会被调用
  • 当前路由query变更时,该守卫会被调用。
beforeRouteLeave

导航离开该组件的对应路由时调用,可以访问组件实例this,参数包括to,from,next。

2.9.4 路由跳转流程

  • 1、导航被触发。
  • 2、在失活的组件里调用 beforeRouteLeave 守卫。
  • 3、调用全局的 beforeEach 守卫。
  • 4、在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  • 5、在路由配置里调用 beforeEnter。
  • 6、解析异步路由组件。
  • 7、在被激活的组件里调用 beforeRouteEnter。
  • 8、调用全局的 beforeResolve 守卫 (2.5+)。
  • 9、导航被确认。
  • 10、调用全局的 afterEach 钩子。
  • 11、触发 DOM 更新。
  • 12、用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

2.10 路由懒加载

2.10.1 为什么需要使用路由懒加载

vue & webpack 的开发模式如果网站内容特别多,会导致 SPA 打出来的包的大小非常的大,经常超过 1M,因此一般会使用 webpack 的 code spliting 把代码分割成不同的 chunk。

按需加载 vue 支持异步组件的方式,因此 vue-router 提供了路由懒加载,可以在路由切换之后再去请求相关路由组件的资源,从而在某种程度上减少 bundle 的大小。

这样可以给客户更好的客户体验,首屏组件加载速度更快一些,解决白屏问题。

2.10.2 定义

路由懒加载简单来说就是按需加载或者延迟加载,即在需要的时候进行加载。

2.10.3 使用

Vue Router 提供了很简单的配置方式,来允许我们把不同路由对应的组件分割成不同的代码块。当对应的路由被访问的时候,Vue Router 才会加载对应组件,这样就能大大减小首页的代码包大小,加快加载速度。使用方式是:
(1) 将异步组件定义为返回一个 Promise 的工厂函数,该函数返回的 Promise 需要 resolve 组件本身。
(2) 使用动态import语法来定义代码分块点(依赖了 Webpack 的代码分割功能)。

ES 提出的import方法(------最常用------)

// 不会被打包到主包中,当匹配到对应的路由时候,才会被加载
const Page2 = () => import("./Page2.vue");

如果我们需要把几个组件都打包到一起,使用相同的webpackChunkName就可以实现:

const Page1 = () => import(/* webpackChunkName: "page" */ "./Page1.vue");
const Page2 = () => import(/* webpackChunkName: "page" */ "./Page2.vue");

2.11 路由元信息

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

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

那么如何访问这个 meta 字段呢?
我们称每个路由对象为路由记录,路由记录可以嵌套。所以到我们匹配到一个路有的时候他有可能有多条路由记录。路由记录会暴露在对应路由对象上,我们可以通过 r o u t e . m a t c h e d 获 取 到 当 前 路 由 所 有 的 路 由 记 录 , route.matched获取到当前路由所有的路由记录, route.matchedroute.matched[n].meta可以获取其中一个路由记录的meta字段。

2.11.1 vue-router中的元信息meta的妙用

{
    path:"/test",
    name:"test",
    component:()=>import("@/components/test"),
    meta:{
        title:"测试页面", //配置title
        keepAlive: true //是否缓存
    }
}

1、配置此路由的标题title

//main.js中的代码
router.beforeEach((to,from,next)=>{
    if(to.meta.title){
        document.title=to.meta.title
    }
    next()
})

2、配置组件是否需要缓存

<!-- app.vue中的代码 -->
<!-- 需要被缓存的路由入口 -->
<keep-alive>  
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>

<!-- 不需要被缓存的路由入口 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>

3 路由实现

前端路由的实现,一般包括两种模式: Hash 模式和History模式。

3.1 Hash和History模式的区别

不管那种模式,都是客户端路由的实现方式,也就是当路径发生变化的时候,不会像服务器发送请求,使用js监视路径的变化,根据不同的地址渲染不同的内容。如果需要服务端内容的话,需要发送Ajax请求来获取。

3.1.1 表现形式的区别

  • Hash模式
  • https://music.163.com/#/playlist?id=4343424324
    hash链接中带有#号,#号后面的内容作为路由地址,可以通过?携带url参数。
  • History模式
  • https://music.163.com/playlist/4343424324
    history模式需要服务器端进行额外的配置支持。

3.1.2 原理的区别

参考下面

3.2 History 模式

History 的路由模式,依赖了一个关键的属性window.history
window.history是一个只读属性,用来获取 History 对象的引用。History 对象提供了操作浏览器会话历史(浏览器地址栏中访问的页面,以及当前页面中通过框架加载的页面)的接口,使用window.history我们可以实现以下与路由相关的重要能力:
(1) 在 history 中跳转。
使用window.history.back()window.history.forward()window.history.go()方法来完成在用户历史记录中向后和向前的跳转。

(2) 添加和修改历史记录中的条目。
HTML5 引入了history.pushState()history.replaceState()方法,它们分别可以添加和修改历史记录条目。这两个 API 都会操作浏览器的历史栈,而不会引起页面的刷新。区别在于,pushState()会增加一条新的历史记录,而replaceState()则会替换当前的历史记录:

/**
 * parameters
 * @state {object} 状态对象 state 是一个 JavaScript 对象,一般是JSON格式的对象字面量
 * @title {string} 可以理解为 document.title,在这里是作为新页面传入参数的
 * @url {string} 该参数定义了增加或改变的历史 URL 记录,可以是相对路径或者绝对路径,url的具体格式可以自定
 */
history.pushState(state, title, url); // 向浏览器历史栈中增加一条记录
history.replaceState(state, title, url); // 替换历史栈中的当前记录

(3) 监听 popstate 事件。
当同一个页面在历史记录间切换时,就会产生popstate事件,popstate事件会被传递给window对象,所以页面路由切换通常会与window.onpopstate配合使用。
上面介绍的history.pushState()或者history.replaceState()调用不会触发popstate事件,popstate事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在 JavaScript 中调用history.back()、history.forward()、history.go()方法)。所以我们可以结合popstate事件pushState()replaceState()来完成完整的路由监听和修改能力。

如果当前处于激活状态的历史记录条目是由history.pushState()方法创建,或者由history.replaceState()方法修改过的, 则popstate事件对象的state属性包含了这个历史记录条目的state对象的一个拷贝。我们来看看示例:

// 假如当前网页地址为http://example.com/example.html
window.onpopstate = function(event) {
  alert(
    "location: " + document.location + ", state: " + JSON.stringify(event.state)
  );
};
//绑定事件处理函数
//添加并激活一个历史记录条目 http://example.com/example.html?page=1,条目索引为1
history.pushState({ page: 1 }, "title 1", "?page=1");
//添加并激活一个历史记录条目 http://example.com/example.html?page=2,条目索引为2
history.pushState({ page: 2 }, "title 2", "?page=2");
//修改当前激活的历史记录条目 http://ex..?page=2 变为 http://ex..?page=3,条目索引为3
history.replaceState({ page: 3 }, "title 3", "?page=3");

history.back(); // 弹出 "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // 弹出 "location: http://example.com/example.html, state: null
history.go(2); // 弹出 "location: http://example.com/example.html?page=3, state: {"page":3}

3.3 Hash模式

Hash 模式主要依赖 Location 对象的 hash 属性(location.hash)和hashchange事件,我们来分别看一下。
(1) Location 对象。
window.location用来获取 Location 对象的引用。Location 对象存储在 Window 对象的 Location 属性中,表示当前 Web 地址。

Location属性描述示例:https://www.test.com/en-US/search?q=URL#search-results
hash设置或返回从井号(#)开始的 URL(锚)#search-results
host设置或返回主机名和当前 URL 的端口号www.test.com
hostname设置或返回当前 URL 的主机名www.test.com
href设置或返回完整的 URLhttps://www.test.com/en-US/search?q=URL#search-results
pathname设置或返回当前 URL 的路径部分/en-US/search
port设置或返回当前 URL 的端口号默认 80 端口
protocol设置或返回当前 URL 的协议https:
search设置或返回从问号(?)开始的 URL(查询部分) ?q=URL?q=URL

location.hash的设置和获取,并不会造成页面重新加载,利用这一点,我们可以记录页面关键信息的同时,提升页面体验。

(2) hashchange 事件。
当一个窗口的 hash 改变时就会触发hashchange事件。hashchange事件对象有两个属性,newURL为当前页面新的 URL,oldURL为当前页面旧的 URL。
Hash 模式通过window.onhashchange监听基于 hash 的路由变化,来进行页面更新处理的。部分浏览器不支持onhashchange事件,我们可以自行使用定时器检测和触发的方式来进行兼容:

(function(window) {
  // 如果浏览器原生支持该事件,则退出
  if ("onhashchange" in window.document.body) {
    return;
  }
  var location = window.location,
    oldURL = location.href,
    oldHash = location.hash;
  // 每隔100ms检测一下location.hash是否发生变化
  setInterval(function() {
    var newURL = location.href,
      newHash = location.hash;
    // 如果hash发生了变化,且绑定了处理函数...
    if (newHash != oldHash && typeof window.onhashchange === "function") {
      // 执行事件触发
      window.onhashchange({
        type: "hashchange",
        oldURL: oldURL,
        newURL: newURL
      });
      oldURL = newURL;
      oldHash = newHash;
    }
  }, 100);
})(window);

3.4 路由的实现原理

3.4.1 路由实现步骤

一般来说,路由都是通过 History API、Location API 和相关的事件监听实现。我们以 Hash 模式为例,如果要实现路由变化的时候加载相应的内容,步骤大概分为三步:
(1)设置监听器,监听popstate或者hashchange事件。
(2)通过 hash(location.href.hash)获取当前的路由位置。
(3)根据当前匹配路径,判断后加载对应模块。

3.4.2 Vue Router 实现

Vue Router 甚至兼容了 Node.js 服务端的情况,它提供的路由模式包括三种:

路由模式说明示例
hash使用 URL hash 值来作路由(支持所有浏览器,包括不支持 HTML5 History Api 的浏览器)a.com/#/pageone
history充分利用history.pushState API 来完成 URL 跳转而无须重新加载页面a.com/pageone
abstract支持所有 JavaScript 运行环境,如 Node.js 服务器端(在 Node.js 端会自动设置该模式为默认值)
如果发现没有浏览器的 API,路由会自动强制进入这个模式-

需要注意的地方是,History 模式需要依赖 HTML5 History API(IE10 以上)和服务器配置。**你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个index.html页面,这个页面就是你 app 依赖的页面。**当浏览器接收到index.html这个页面之后,再去判断路由地址,然后根据路由地址/login去加载login这个组件,这是它执行的一个过程。所以如果觉得 hash 片段可以接受(有些人会觉得太丑了无法接受),可以优先选择 Hash 模式来进行开发,简单方便。

vue-cli中自带的服务器,已经做了history的配置。

我们看看 Vue Router 中的 History 对象:

// HTML5 History
export class HTML5History extends History {
  constructor(router: Router, base?: string) {
    super(router, base);

    const expectScroll = router.options.scrollBehavior;
    const supportsScroll = supportsPushState && expectScroll;

    if (supportsScroll) {
      setupScroll();
    }

    const initLocation = getLocation(this.base);
    // 添加事件监听
    window.addEventListener("popstate", e => {
      const current = this.current; // History路由因为异步防护,不会更新

      // 首先避免在某些浏览器中调度第一个`popstate`事件
      const location = getLocation(this.base);
      if (this.current === START && location === initLocation) {
        return;
      }

      // 该方法进行路由的更新、对应钩子逻辑的执行、处理完毕的回调
      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true);
        }
      });
    });
  }
  // 前进到某个位置
  go(n: number) {
    window.history.go(n);
  }
  // 添加到路由
  push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this;
    // 前置逻辑执行
    this.transitionTo(
      location,
      route => {
        // 添加到历史记录中
        pushState(cleanPath(this.base + route.fullPath));
        handleScroll(this.router, route, fromRoute, false);
        // 回调
        onComplete && onComplete(route);
      },
      onAbort
    );
  }
  // 更新当前路由
  replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this;
    this.transitionTo(
      location,
      route => {
        replaceState(cleanPath(this.base + route.fullPath));
        handleScroll(this.router, route, fromRoute, false);
        onComplete && onComplete(route);
      },
      onAbort
    );
  }
  // 确认是否某个路由
  ensureURL(push?: boolean) {
    if (getLocation(this.base) !== this.current.fullPath) {
      const current = cleanPath(this.base + this.current.fullPath);
      push ? pushState(current) : replaceState(current);
    }
  }
  // 获取当前路由
  getCurrentLocation(): string {
    return getLocation(this.base);
  }
}

我们再来看看 Hash 对象:

export class HashHistory extends History {
  constructor(router: Router, base: ?string, fallback: boolean) {
    super(router, base);
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return;
    }
    ensureSlash();
  } // 以避免过早地触发hashchange侦听器

  // 这会延迟到应用程序安装完毕
  setupListeners() {
    const router = this.router;
    const expectScroll = router.options.scrollBehavior;
    const supportsScroll = supportsPushState && expectScroll;

    if (supportsScroll) {
      setupScroll();
    }
    // 添加事件监听
    // 优先使用 popstate,同时使用 hashchange 兜底
    window.addEventListener(
      supportsPushState ? "popstate" : "hashchange",
      () => {
        const current = this.current;
        if (!ensureSlash()) {
          return;
        }
        // 该方法进行路由的更新、对应钩子逻辑的执行、处理完毕的回调
        this.transitionTo(getHash(), route => {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true);
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath);
          }
        });
      }
    );
  }
  // 前进到某个位置
  go(n: number) {
    window.history.go(n);
  }
  // 添加到路由
  push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this;
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath);
        handleScroll(this.router, route, fromRoute, false);
        onComplete && onComplete(route);
      },
      onAbort
    );
  }
  // 更新当前路由
  replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this;
    this.transitionTo(
      location,
      route => {
        replaceHash(route.fullPath);
        handleScroll(this.router, route, fromRoute, false);
        onComplete && onComplete(route);
      },
      onAbort
    );
  }
  // 确认是否某个路由
  ensureURL(push?: boolean) {
    const current = this.current.fullPath;
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current);
    }
  }
  // 获取当前路由
  getCurrentLocation() {
    return getHash();
  }
}

3.5 Vue Router的模拟实现

实现思路:
(1)创建LVueRouter插件,静态方法install

  • 判断当前插件是否已经被加载
  • 当Vue加载的时候把换入的router对象挂载到Vue实例上(注意:只执行一次)
    (2) 创建LVueRouter类
  • 初始化options、routeMap、app(简化操作,创建Vue实例作为响应式数据记录当前路径)
  • initRouteMap()遍历所有的路由信息,把组件和路由的映射记录到routeMap对象中
  • 创建router-link和router-view组件
  • 当路径发生改变的时候通过当前路径在routerMap对象中找到对应的组件,渲染router-view

创建LVueRouter插件

export default class VueRouter {
	static install (Vue) {
		static install (Vue) { // 如果插件已经安装直接返回 
		if (VueRouter.install.installed && _Vue === Vue) return
		
		VueRouter.install.installed = true
		_Vue = Vue
		
		Vue.mixin({
			beforeCreate () {
				// 判断 router 对象是否已经挂载了 Vue 实例上
				if (this.$options.router) {
					// 把 router 对象注入到 Vue 实例上
					_Vue.prototype.$router = this.$options.router
				}
			}
		})
	}
}

实现 LVueRouter 类 - 构造函数

constructor (options) {
	this.options = options
	// 记录路径和对应的组件
	this.routeMap = {}
	this.app = new _Vue({
		data:{
			//当前默认路径
			current:'/'
		}
	})
}

实现 LVueRouter 类 - initRouteMap()

initRouteMap () {
	// routes => [{ name: '', path: '', 		component: }]
	// 遍历所有的路由信息,记录路径和组件的映射
	this.options.routes.forEach(route => {
		// 记录路径和组件的映射关系
		this.routeMap[route.path] = route.component
	})
}

实现 LVueRouter 类 - 注册事件

initEvents(){
	//当路径变化之后,重新获取当前路径并记录到 current
	window.addEventListener('hashchange', this.onHashChange.bind(this))
	window.addEventListener('load', this.onHashChange.bind(this))
	
}

onHashChange () {
	this.app.current = window.location.hash.substr(1) || '/'
}

实现 LVueRouter 类 - router-link 和 router-view 组件

initComponents () {
	_Vue.component('RouterLink', {
		props:{
			to:String
		},
		//需要带编译器版本的 Vue.js
		//template: "<a :href='\"#\" + to'><slot></slot></a>"
		//使用运行时版本的 Vue.js
		render(h) {
			return h('a',{
				attrs:{
					href:'#' + this.to
				}
			},[this.$slots.default])
		}
	})
	const self = this 
	
	_Vue.component('RouterView',{
		render(h) {
			//根据当前路径找到对应的组件,注意 this 的问题
			const component = self.routeMap[self.app.current]
			return h(component)
		}
	})
}

注意:
vue-cli创建的项目默认使用的运行时版本的Vue.js
如果想要切换成编译器版本的Vue.js,需要修改vue-cli配置
项目根目录创建vue.config.js文件,添加runtimeCompiler

module.exports = {
	runtimeCompiler : true
}

实现LVueRouter类–init

init() {
	this.initRouteMap()
	this.initEvents()
	this.initComponents()
}

//插件的install方法中调用init()初始化
if(this.$options.router){
	_Vue.prototype.$router = this.$option.router
	//初始化插件的时候,调用init
	this.$options.router.init()
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值