Vue-Router 原理实现

 

一.Vue-Router基本使用

在main.js中会将router对象注入到vue实例中,注入vue实例后会创建两个属性$router和$router。第一个是路由规则,第二个为路由实例

在路由规则中可以获取路由信息,在路由实例中可以获得路由方法比如push、go、replace等。此外若在开发插件过程中想要获取路由规则可以通过路由实例中的currentRoute获取路由的规则。

1、动态路由

动态路由有两种传参方式一种是直接通过$route.params.id获取到路由中的id占位,另一种是通过在路由规则中开启props,像父子组件传值一样获取到路由占位中的id

// router的index.js中
{
    path: '/blog/:id',
    name: 'Blog',
    // 开启props,会把URL 中的参数传递给组件
    // 组件中通过props来接受URL参数
    props: true,
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/Blog.vue')
  },
<template>
  <div class="about">
    <div>
      <!-- 方式1:通过当前路由规则,获取数据 -->
      通过当前路由规则获取:{{ $route.params.id }}

      <br/>
      <!-- 方式2: 路由规则中开启props传参 -->
      通过开启 props 获取:{{ id }}
    </div>
  </div>
</template>

<script>
export default {
  name: 'Blog',
  props: ['id']
}
</script>

2、嵌套路由

若两个页面例如首页和详情页拥有相同的头部和尾部,则他们可以抽成一个例如Layout组件进行和并,在相同的头与尾中间用<router-view/>进行路由替换

在嵌套路由中,外部Layout中的路径会和children里面的路径进行合并,同时加载Layout组件和Index组件将他们合并到一起。在渲染的时候,会先加载Layout然后再加载Index然后合并在一起一同渲染出来

 //嵌套路由
  {
    path: '/',
    component: Layout,
    children: [
      {
        name: 'index',
        path: '',
        component: Index
      },
      {
        name: 'detail',
        path: 'detail/:id',
        props: true,
        component: () => {'@/views/Detail.vue'}
      }
    ]
  }

3、Vue-Router编程式导航

(1)、push方法

push() {
      this.$router.push('/')
    },

(2)、replace方法

replace() {
      this.$router.replace('/')
    },

replace方法和push方法类似都可以跳转到相应的组件,但是replace不会记录跳转的历史,也就是说浏览器依然不能后退到上一个跳转过来的页面

(3)、go方法

go() {
      this.$router.go(-2)
    }

go方法是跳转到历史中的某一次,它可以是负数,也就是意味着后退,例如go(-2)后退两次记录

(4)传递参数

goDetail() {
      this.$router.push({name: 'Detail', params:{ id: 1 }})
    },

传递参数跳转先指定跳转组件的名称,在去传递参数

二、Hash和History模式区别

表现形式的区别:

  •        Hash模式:https://www.baidu.com/#/newsList?id=123456
  •        History模式: https://www.baidu.com/newsList/123456

Hash模式在#后面是url的地址同时会有一些和url无关的符号比如问好

History模式就是正常的url。

原理的区别:

  • Hash模式是基于锚点,以及onhashchange事件

通过锚点的值作为路由地址,当地址发生变化的时候触发onhashchange事件,根据路径决定页面上呈现的内容

  • History模式是基于Html5中的History API

History模式是基于history.pushState()和history.replaceState()两个方法。history.push和history.pushState方法的区别是push方法会在路径发生变化的时候向服务端发送请求,而pushState不会向服务端发送请求只是改变浏览器中地址栏的地址,然后记录地址内容。另外pushState方法是IE10之后才支持的会出现兼容性问题。

单页面应用history页面刷新流程:在某一个详情页面刷新后,先去服务器端请求这个页面,当服务器支持history路由模式时,因为Vue是一个单页面应用,请求的详情页面没有在服务器资源中找到则会默认返回index.html到浏览器,当浏览器接收到后会判断具体的路由地址然后去渲染某一个组件。

三、Vue-Router实现原理

回顾核心代码:

// router/index.js
// 注册插件
Vue.use(VueRouter)
// 创建路由对象
const router = new VueRouter({
  routes: [
    { name: 'home', path: '/', component: homeComponent }
  ]
})

// main.js
// 创建 Vue 实例, 注册 router 对象
new Vue ({
  router,
  render: h => h(App)
}).$mount('#app')

1.Vue.use这个方法它可以传入函数或者是对象,如果传入的是方法那么Vue会直接调用这个方法,如果传入的是一个对象那么他会去调用这个对象的install方法,这里面传入的是一个对象,想要实现Vue-Router就要去实现他的install方法

2.第二步,要去创建一个实例,所以VueRouter应该是一个构造函数或者是一个类,此处就应该要用类的方法去实现,并且此处还要有一个静态的install方法,因为这里是直接把VueRouter传入给Vue.use方法,类也是一个对象,因此要实现VueRouter是什么就清楚了:他是一个类,在这个类的里面有一个install的静态方法,VueRouter的构造函数需要传入一个参数并且该参数是一个对象形式,在对象中是一个路由规则,规则中最重要的是路由的地址和对应的组件

3.最后一步。是创建一个Vue实例并传入我们刚才创建好的router对象

(一)、实现VueRouter-install方法

let _Vue = null // 全局变量
export default class VueRouter {
  // vuerouter类中的静态函数intsall函数传递两个参数 
  // Vue.use() 在调用install方法时 会传递两个参数 一个是Vue的构造函数 一个是可选的选项对象
  // 这里不需要选项对象就不需要传递了
  static install(Vue) {
    //在这个静态方法里需要干哪些事情
    // 1、要判断当前插件是否已经被安装,如果已经被安装那么就不需要重复去安装了
    if(VueRouter.install.installed) {
      return 
    }
    VueRouter.install.installed = true
    // 2、把Vue的构造函数记录到全局变量中来,因为当前方法是个静态方法而在这个方法中接受了Vue的构造函数,而以后我们的实例方法也要用到
    //(接上)Vue的全局构造函数。
    _Vue = Vue
    // 3、把注册Vue实例时传入的router对象给他注入到所有的vue实例上,平时使用的this.$router,就是在这个第三步注入到Vue实例上的,并且我们所有
    //(接上)的组件也全都是Vue的实例
    // (如果想让所有的实例都共享一个成员方法那么就要用原型的形式)
    // 混入(为了this能指向Vue实例) 给所有实例都混入一个beforeCreate钩子函数,在这个钩子函数中就可以获取到Vue实例,然后给他的原型上注入$router这个成员属性
    // 因为每一个组件都是一个Vue原型实例,所以之后每一个组件都会执行beforeCreate这个钩子函数
    _Vue.mixin({
      beforeCreate() {
        // 因为获取的是传入的选项router 所以获取的是实例的选项this.$options,从选项中获取router这个属性
        // 因为混入每一个组件都会执行一次这个钩子会执行很多次,这里要做一个判断只有给实例传入router的时候才会执行,如果不传入则视为普通组件就不知行了
        if(this.$options.router) {
          _Vue.prototype.$router = this.$options.router
        }
      }
    })
  }
}

(二)、VueRouter的构造函数

constructor (options) {
    // options传入的是路由规则
    this.options = options
    // routeMap是一个键值对对象,用来解析传入的路由规则。键为路由地址,值为路由组件之后会根据路由地址在routeMap中找到对应的路由组件然后渲染到浏览器
    this.routeMap = {}
    // data是一个响应式的的对象里面current属性存储当前的路由地址,Vue中的observable方法可以创建一个响应式的属性
    this.data = _Vue.observable({
      current: '/' //默认是/根目录
    })
  }

(三)、VueRouter-- createRouteMap方法

createRouteMap () {
    //遍历所有的路由规则,把路由规则解析成键值对的形式存储到routeMap中
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

(四)、 VueRouter-- initComponent方法

initComponents (Vue) {
    // 创建router-link组件,用Vue的component方法创建一个组件,第一个传入的是组件名字,第二个传入的是可选选项
    Vue.component('router-link',{
      // router-link会接收一个传入的属性to 外部传入的属性用props来接受,传入的名字是to属性是string字符串
      props: {
        to: String
      },
      // h函数去创建虚拟dom
      render (h) {
        // h函数会传递三个值 一个是选择器这里是A标签第二个是创建的dom对象设置的一些属性,通过attrs这个属性设置,第三个是内容传递的默认没有取名字的插槽
        return h('a', {
        // 要阻止超链接的后续点击事件(默认行为),因为跳转只想让标签地址改变不与服务器进行连接跳转,只改变浏览器地址栏且记录历史采用pushState方法
          attrs: {
            href: this.to
          },
          // 设置属性用attrs,设置事件用on
          on: {
            click: this.clickHandler // 不加小括号 加小括号是调用事件,不加是注册
          }
          // Vue实例的$slots方法获取插槽名字.default获取默认插槽
        }, [this.$slots.default])
      },
      methods: {
        // 这个点击事件处理两件事,第一件事是调用pushState方法改变浏览器的地址栏且不向服务器发送请求,第二件事是加载对应的组件
        clickHandler (e) {
          // pushState传入三个参数第一个是传给后续popState的对象参数 第二个是网页标题 第三个是要跳转的地址
          history.pushState({}, '', this.to)
          //加载对应组件 router-link是一个组件也是一个实例,在注册插件的时候已经把$router这个对象挂载到构造函数的原型属性上,所以所有的Vue实例都可以访问$router这个对象
          // 因为data是一个响应式的属性所以当他的值改变,同时视图也发生改变重新加载router-view组件吧对应的组件渲染到视图上来
          this.$router.data.current = this.to
          // 这个方法阻止事件的默认行为
          e.preventDefault()
        } 
      }
      // template最终会渲染成一个a标签,href绑定到上面传入的to,中间用插槽占位替换链接的文字
      // template: '<a :href="to"><slot></slot></a>'
    })
    const self = this
    Vue.component('router-view', {
      render (h) {
        // 寻找当前路由地址对应的组件
        const component = self.routeMap[self.data.current]
        // h函数可以把我们的组件转换成虚拟dom然后返回
        return h(component)
      }
    })
  }

(五)VueRouter--initEvent

initEvent () {
    // 这个方法用来注册popState事件,处理浏览器点击前进和后退改变路由加载对应组件的方法
    // 第一个值是事件名称 第二个是事件处理函数
    window.addEventListener('popstate', () => {
      // 让响应式里的路径等于现在浏览器地址栏里面的路径,让他发生改变
      this.data.current = window.location.pathname
    })
  }

完整代码:

let _Vue = null // 全局变量
export default class VueRouter {
  // vuerouter类中的静态函数intsall函数传递两个参数 
  // Vue.use() 在调用install方法时 会传递两个参数 一个是Vue的构造函数 一个是可选的选项对象
  // 这里不需要选项对象就不需要传递了
  static install(Vue) {
    //在这个静态方法里需要干哪些事情
    // 1、要判断当前插件是否已经被安装,如果已经被安装那么就不需要重复去安装了
    if(VueRouter.install.installed) {
      return 
    }
    VueRouter.install.installed = true
    // 2、把Vue的构造函数记录到全局变量中来,因为当前方法是个静态方法而在这个方法中接受了Vue的构造函数,而以后我们的实例方法也要用到
    //(接上)Vue的全局构造函数。
    _Vue = Vue
    // 3、把注册Vue实例时传入的router对象给他注入到所有的vue实例上,平时使用的this.$router,就是在这个第三步注入到Vue实例上的,并且我们所有
    //(接上)的组件也全都是Vue的实例
    // (如果想让所有的实例都共享一个成员方法那么就要用原型的形式)
    // 混入(为了this能指向Vue实例) 给所有实例都混入一个beforeCreate钩子函数,在这个钩子函数中就可以获取到Vue实例,然后给他的原型上注入$router这个成员属性
    // 因为每一个组件都是一个Vue原型实例,所以之后每一个组件都会执行beforeCreate这个钩子函数
    _Vue.mixin({
      beforeCreate() {
        // 因为获取的是传入的选项router 所以获取的是实例的选项this.$options,从选项中获取router这个属性
        // 因为混入每一个组件都会执行一次这个钩子会执行很多次,这里要做一个判断只有给实例传入router的时候才会执行,如果不传入则视为普通组件就不知行了
        if(this.$options.router) {
          _Vue.prototype.$router = this.$options.router
          // 当插件注册完成后调用初始化的方法---解析键 值对、创建router-link组件
          this.$options.router.init()
        }
      }
    })
  }

  constructor (options) {
    // options传入的是路由规则
    this.options = options
    // routeMap是一个键值对对象,用来解析传入的路由规则。键为路由地址,值为路由组件之后会根据路由地址在routeMap中找到对应的路由组件然后渲染到浏览器
    this.routeMap = {}
    // data是一个响应式的的对象里面current属性存储当前的路由地址,Vue中的observable方法可以创建一个响应式的属性
    this.data = _Vue.observable({
      current: '/' //默认是/根目录
    })
  }

  init () {
    // 把两个初始化用的组件包裹起来让外界使用更方便
    this.createRouteMap()
    this.initComponents(_Vue)
    this.initEvent()
  }

  createRouteMap () {
    //遍历所有的路由规则,把路由规则解析成键值对的形式存储到routeMap中
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

  initComponents (Vue) {
    // 创建router-link组件,用Vue的component方法创建一个组件,第一个传入的是组件名字,第二个传入的是可选选项
    Vue.component('router-link',{
      // router-link会接收一个传入的属性to 外部传入的属性用props来接受,传入的名字是to属性是string字符串
      props: {
        to: String
      },
      // h函数去创建虚拟dom
      render (h) {
        // h函数会传递三个值 一个是选择器这里是A标签第二个是创建的dom对象设置的一些属性,通过attrs这个属性设置,第三个是内容传递的默认没有取名字的插槽
        return h('a', {
        // 要阻止超链接的后续点击事件(默认行为),因为跳转只想让标签地址改变不与服务器进行连接跳转,只改变浏览器地址栏且记录历史采用pushState方法
          attrs: {
            href: this.to
          },
          // 设置属性用attrs,设置事件用on
          on: {
            click: this.clickHandler // 不加小括号 加小括号是调用事件,不加是注册
          }
          // Vue实例的$slots方法获取插槽名字.default获取默认插槽
        }, [this.$slots.default])
      },
      methods: {
        // 这个点击事件处理两件事,第一件事是调用pushState方法改变浏览器的地址栏且不向服务器发送请求,第二件事是加载对应的组件
        clickHandler (e) {
          // pushState传入三个参数第一个是传给后续popState的对象参数 第二个是网页标题 第三个是要跳转的地址
          history.pushState({}, '', this.to)
          //加载对应组件 router-link是一个组件也是一个实例,在注册插件的时候已经把$router这个对象挂载到构造函数的原型属性上,所以所有的Vue实例都可以访问$router这个对象
          // 因为data是一个响应式的属性所以当他的值改变,同时视图也发生改变重新加载router-view组件吧对应的组件渲染到视图上来
          this.$router.data.current = this.to
          // 这个方法阻止事件的默认行为
          e.preventDefault()
        } 
      }
      // template最终会渲染成一个a标签,href绑定到上面传入的to,中间用插槽占位替换链接的文字
      // template: '<a :href="to"><slot></slot></a>'
    })
    const self = this
    Vue.component('router-view', {
      render (h) {
        // 寻找当前路由地址对应的组件
        const component = self.routeMap[self.data.current]
        // h函数可以把我们的组件转换成虚拟dom然后返回
        return h(component)
      }
    })
  }

  initEvent () {
    // 这个方法用来注册popState事件,处理浏览器点击前进和后退改变路由加载对应组件的方法
    // 第一个值是事件名称 第二个是事件处理函数
    window.addEventListener('popstate', () => {
      // 让响应式里的路径等于现在浏览器地址栏里面的路径,让他发生改变
      this.data.current = window.location.pathname
    })
  }
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值