模拟实现 Vue Router

555 阅读2分钟

本文想通过模拟VueRouter来看一下他实现的原理, 此处使用history模式来模拟。

创建Vue项目

  • 首先我们通过vue create vueApp 来创建一个vue的项目,
  • 进入vueApp 后执行vue add router 载入vue-router
  • 执行npm run serve打开浏览器,发现我们的项目已经初始化完成了

Vue Router 分析

首先我们对Vue Router功能进行简单的分析:

  1. 他是前端路由,当路径切换的时候,在浏览器端判断路径,并加载当前路径对应的组件
  2. 他有两种模式,一种是hash,一种是history
    • hash模式是基于锚点,以及onhashchange事件
    • history模式是基于html5只不过的History API
      • history.pushState() IE10以后才支持
      • history.replaceState()
    • history简单的工作流程
      • 通过 history.pushState()方法改变地址栏
      • 监听 popstate 事件
      • 根据当前路由地址找到对应组件重新渲染
  3. 接下来我们去项目中看下vue-router 是如何使用的

  1. 下面是VueRouter的一个类图,我们接下来根据这个类图来实现VueRouter

模拟实现Vue Router

回到刚创建的vueApp项目中,在src下创建一个vuerouter文件夹,并添加index.js在这个文件夹,此文件夹存放我们自己写的vue-router模块,在index.js中进行开发

install 方法

install的分析

  1. Vue.use()调用VueRouter时会传递两个参数,一个是Vue的构造函数,第二个是可选的选项对象(本文中没用到这个参数)
  2. install是一个静态方法静态方法,接收VueRouter传递的两个参数
  3. 函数内部首先判断下这个插件是否被安装
  4. 在全局作用域储存Vue构造函数(后面还需要调用Vue构造函数)
  5. 把创建Vue实例时传入的router对象,注入到所有Vue实例上(this.$router就是在这个时候注册到Vue实例上的) 代码实现
let _Vue = null
export default class VueRouter {
  static install(Vue){
    if(VueRouter.install.installed) {
      return
    }
    VueRouter.install.installed = true
    _Vue = Vue
    _Vue.mixin({
      beforeCreate(){
        if(this.$options.router){
          _Vue.prototype.$router = this.$options.router;
          this.$options.router.init() // 调用init()方法,此方法定义在下文中可以看到
        }
      }
    })
  }
}

构造函数

构造函数分析

  1. 观察上文中的类图,发现构造函数接收一个参数options
  2. 在构造函数中初始化类图中的三个属性
    • options:存储传入的参数options
    • routeMap: 存储键值对,值可以从传入options中routes里面中拿到,键:path, 值:component
    • data 是一个响应式对象,里面current存放当前的路由地址 代码实现
constructor(options) {
  this.options = options
  this.routeMap = {} 
  this.data =  _Vue.ovservable({
    current: '/'
  })
}

createRouteMap

函数分析

  1. 遍历所有的路由规则(options.route),把路由规则转换成上文说的键值对形式 存在routeMap对象中去 代码实现
createRouteMap(){
  this.options.routes.forEach(route => {
    this.routeMap[route.path] = route.component
  })
}

initComponents

函数分析

  1. 这个方法接收一个Vue构造函数(也可以不去传这个参数,因为我们在全局已经存储到_Vue了,这样做的目的是为了减少此模块对其他模块的依赖)
  2. 这个方法是创建router-link组件
    • router-link在使用的时候,传入了to属性
    • router-link最终渲染的时候是一个a标签
    • router-link中间会有一些展示内容

代码实现

🌟 template参数方式创建会有些小坑,后面会说到

initComponents(Vue){
  Vue.component('router-link',{
    props: {
      to: String,
    },
    template: '<a :herf="to"><slot></slot></a>' 
  })
}

init函数

这个函数没什么特别的 就是帮助我们执行一些初始化的方法

init(){
  this.createRouteMap()
  this.initComponents(_Vue)
}
// 在instll中调用,调用位置参看上面的install方法
  • 初始化完成后去修改router.js文件中导入VueRouter的模块,然后重新运行项目
  • 运行后报错,You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build . 我们正在使用只包含运行版本的Vue,模板编译器不可用,这里就引出了关于Vue构建版本的问题

Vue构建版本

  • 运行时版: 不支持template模板,需要打包的时候提前编译
  • 完整版:包含运行时编译器的完整版,体积比运行时版本大10K左右,程序运行的时候把模板转换成render函数

解决方法

🎁 来解决上面说的template参数的坑啦,两种方式解决:1.使用Vue完整版 2.使用render函数

  • 使用Vue完整版 思路就是将当前运行时版改成完整版,创建vue.config.js
module.exports={
  runtimeCompiler: true // 启动包含运行时编译器的完整版
}

这个方法主要是开启完整版,对于配置的具体说明本文就不过多赘述,详情可以参考vue官方文档

  • 使用render函数
initComponents(Vue){
  Vue.component('router-link',{
    props: {
      to: String,
    },
    render(h){
      return h('a', {
        attrs: {
          herf: this.to
        },
      },[this.$slots.default]);
    }
    // template: '<a :herf="to"><slot></slot></a>'
  })
}

router-view组件

组件分析

  • 相当于一个占位符,在这个组件内部,根据当前的路由地址获取到对应的组件,并渲染到router-view的位置
  • 在initComponents 函数中继续创建router-view组件
  • 上面我们已经知道如何去创建一个组件,现在思考一下在创建组件的render函数中需要进行什么样的操作
    • 获取到当前的路由地址,根据路由地址在routeMap中找到对应的组件
    • 然后调用h函数帮我们把找到的组件转成虚拟dom返回
initComponents(Vue){
  Vue.component('router-link',{
    props: {
      to: String,
    },
    render(h){
      return h('a', {
        attrs: {
          herf: this.to
        },
        on: {
          click: this.clickHandler
        }
      },[this.$slots.default]);
    },
    methods: {
      clickHandler(e){
        // 改变浏览器地址栏,但是不会向浏览器发请求
        history.pushState({}, '', this.to)
        // 把当前路径记录到current里面
        this.$router.data.current = this.to
        e.preventDefault();
      }
    }
    // template: '<a :herf="to"><slot></slot></a>'
  })
  const self = this
  Vue.component('router-view',{
    render(h){
      const component = self.routeMap[self.data.current]
      return h(component)
    }
  })

}

initEvent

  • 点击浏览器的前进和后退按钮,地址栏发生了改变,但是视图没有发生变化,下面我来解决这个问题
  • 这个方法的作用是用来注册popstate事件
initEvent(){
  window.addEventListener('popstate',()=>{
    this.data.current = window.location.pathname
  })
}
// 然后在init方法中调用此方法