VueRouter 原理及实践

186 阅读6分钟

VueRouter 的实现原理

-     通过pushState方法改变地址栏并将改变的地址添加到历史记录中
-     通过监听popstate事件在触发事件后更新页面Dom
-     从而实现单页面的路由切换以及页面加载

下面详细讲解一下VueRouter的具体代码实现过程

先梳理一下实现步骤

1.     创建VueRouter类并默认导出
2.     因为Vue.use 会执行类的install方法,所以需要定义一个install静态方法
3.     在构造函数中接收传入的option对象, 并挂载到类上
4.     创建一个用于存储VueRouter当前路径的响应式对象
5.     在实例上挂载一个RouterMap用于记录 VueRouter路径中, path和component的对应关系
6.     将传入的VueRouter对象数组转为key Value对象关系
7.     创建Router-link和Router-View组件,并注册到Vue实例上

初始化代码环境

-     环境是Vue2 加 VueRouter 的vue-cli创建的环境
-     创建src/vueRouter/index.js 用于书写我们自己的VueRouter
-     本次写的示例代码主要替代环境中的Vue-Router来实现Vue的路由切换功能

环境创建选项

1642383242.jpg

书写代码实现步骤

//创建一个RouterVue类并导出
//这里创建一个_Vue用于接收并缓存Vue示例的变量
let _Vue = null  
export default class VueRouter {
};

// 添加一个静态install方法,用于use方法的调用
static install (Vue) {
// 将Vue示例存储下来,以便后面使用
// 这里为什么不直接挂载到VueRouter类上面呢?
// 因为静态方法无法调用this所以无法挂载到类上
_Vue = Vue
}
// 添加constructor 构造函数
constructor (option) {
    // 将options挂载打类上
    this.option = option
    // 添加一个响应式对象用于记录当前VueRouter路由地址
    // 这里会使用到Vue的 observable 方法,它的作用就是将对象转为响应式
    // 官网路径可查看详细说明 https://cn.vuejs.org/v2/api/#Vue-observable
    this.data = _Vue.observable({
      current: '/'
    })
    // routerMap 对象主要用于存储路由的Path,component之间的索引关系
    this.routerMap = {}
    // 我们在构造函数中调用下面的这两个方法
    // 这样在new 操作时构造函数中的代码就都会得到执行
    this.createRouterMap()
    this.initComponets()
}
// 添加createRouterMap 方法
  createRouterMap = () => {
    // 这里功能很简单,主要是将数组改为以对象的形式存储
    this.option.routes.forEach(route => {
      this.routerMap[route.path] = route.component
    })
  }
 
// 添加initComponent 主要用于
// 创建router-link route-view 这两个组件,并注册到Vue的component组件列表中
  initComponets = () => {
    // 这里我们存储当前this以便后面使用
    _Vue.component('router-link', {
      props: 'to',
      template: '<a :href="to"><slot></slot></a>'
    })

    _Vue.component('router-view', {
    // this.routerMap[this.data.current] 所指的是一个Vue文件路径
    // 之所以能够使用render函数进行渲染 是因为Webpack已经将该路径的文件预编译为虚拟Dom
    // 所以这里只需要通过H函数来将虚拟Dom进行挂载
      render (h) {
        // 因为这里获取到的this 是render函数的this,所以我们要改用上面的self
        return h(this.routerMap[this.data.current])
      }
    })
  }
- 上面我们已经将前期的准备工作做完了,那我们如何来将这些方法挂载到Vue实例上呢
- 我们来分析一下步骤
- 首先我们需要获取到Vue的实例才能进行操作,那在哪里能获取到Vue的实例呢
// 我们来观察下面的router/index.js中的代码
import Vue from 'vue'
import VueRouter from '../vueRouter'
import Home from '../views/Home.vue'
// 从这里可以看出来Vue.use()方法是在 VueRouer 执行构造函数之前就执行的
// 而且use方法会调用install方法并将Vue实例传进去
// 只有在这个方法中拿到Vue实例,我们构造函数中执行的代码才有意义
Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

此时基本的条件我们已经写好了,我们将引入的vue-Router改成我们写的VueRouter

// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

// 改成下面的效果
import Vue from 'vue'
import VueRouter from '../vueRouter'
import Home from '../views/Home.vue'

这个时候页面就会出现以下的错误

1642469884(1).jpg

- 这时候我们稳住,不要慌,一个问题一个问题来
- 第一个问题是说我们正在使用的Vue版本是运行时版本,其中模板编译器不可用
- 那为什么会提示这个问题呢?
    _Vue.component('router-link', {
      props: 'to',
      template: '<a :href="to"><slot></slot></a>'
    })
- 是因为我们上面这个地方使用了template模板
- 当前我们的Vue版本是运行时版本,它不认识这个模板,你需要加载完整版Vue才带有编译器
- 不过这样会让Vue的大小增加10k, 一般我们的项目都是使用运行版Vue, 因为有Webpack帮我们预编译
- 所以我们不会关注到这个问题
- 这里我们就先使用完整版Vue来解决这个问题
- 在Vue-cli的配置参考中有说明如何开启完整版Vue   官网路径 https://cli.vuejs.org/zh/config/#runtimecompiler

// 在项目中添加这个文件配置就能开启完整版Vue,就能解决上面的问题了
- // vue.config.js

module.exports = {
   runtimeCompiler: true
}

// 解决了上面的问题,我们再来解决下面的两个问题
// 错误中说没有找到current字段,但是我们分明定义了呀
    _Vue.component('router-view', {
        this.routerMap[this.data.current] 
        render (h) {
          // 因为这里获取到的this 是render函数的this,所以我们要改用上面的self
          return h(this.routerMap[this.data.current])
        }
        })
// 所以我们改成下面这种写法
const self = this
_Vue.component('router-view', {
  render (h) {
    return h(self.routerMap[self.data.current])
  }
})
// 我们再去看页面效果

这里我们可以看到页面已经没有错误了,页面也显示了组件内容了

11111.jpg

- 这个时候虽然可以看到页面效果了,但是点击无法正常的切换页面
- 而且点击浏览器还会转圈,说明我们是向服务器发起请求了, Vue正常页面切换是不会刷新浏览器的所以这也是个问题
// 首先我们来解决一下,上面的问题
_Vue.component('router-link', {
  props: {
    to: String
  },
  // 这里我们给A标签添加一个click事件
  template: '<a :href="to" @click="clickHandle"><slot></slot></a>',
  methods: {

    clickHandle (e) {
      // 这里禁用掉默认事件,这样就不会刷新页面了
      e.preventDefault()
      // 这里我们改变当前VueRouter的路由,因为data是响应式对象
      // 所有就会触发Router-View 进行页面的更新,这样页面就能正常的刷新了
      // 这里还有一个点就是,这里的data不能用this, 因为这里的this是指向当年组件的所以要用上面保留的self
      self.data.current = this.to
      // 其实上面就已经实现了页面的切换, 但是因为切换了页面地址栏的url并没有改变
      // 所以我们这里还需要处理以下地址栏的url来进行变化
      // 这个pushState方法不仅会改变地址栏,还会将添加的地址加入到历史列表中
      window.history.pushState({}, '', this.to)
    }
  }
})

到这里基本路由切换就完成了

1642557319(1).jpg

但是这里还有一个问题

image.png

- 浏览器的这个返回前进这个时候只能改变地址栏的URL, 页面并不会进行切换
- 这里我们来分析以下,这里的前进后退是在历史列表中查找上一个或者下一个
- 它的改变我们写的VueRuoter并不知道,所以才会有这个问题
- 知道问题我们就来解决它

// 因为浏览器在前进和后退的时候都会触发popstate事件
// 所以我们在它触发事件的时候改变我们的当前路由就能实现页面切换了
  initEvent = () => {
    window.addEventListener('popstate', () => {
    // window.location.pathname 这个属性就能拿到地址栏后缀类似 /about 这个路径
      this.data.current = window.location.pathname
    })
  }
  // 然后我们把这个初始化方法加入到构造函数中
   constructor (option) {
    this.option = option
    this.data = _Vue.observable({
      current: '/'
    })
    this.routerMap = {}
    this.createRouterMap()
    this.initComponets()
    this.initEvent()
  }

这个时候我们的VueRouter的基本功能就大功告成了

image.png