Vue Router 原理实现

111 阅读3分钟

Vue 前置知识

  • 插件
  • 混入
  • Vue.observable()
  • 插槽
  • render 函数
  • 运行时和完整版的Vue

Hash 模式

  • URL中# 后面的内容作为路径地址**(改变#后面的内容,浏览器不会向服务器请求,但是会把地址记录到浏览器的访问历史中)**
  • 监听hashchange 事件
  • 根据当前路由地址找到对应组件重新渲染

History 模式

  • 通过history.pushState() 方法改变地址栏**(pushState 方法只是改变地址栏,并把地址记录到浏览器的访问历史中,并不会向服务器发送请求)**
  • 监听popstate 事件(back,forward 或者前进后退按钮时会触发, pushState 和replaceState方法不会触发)
  • 根据当前路由地址找到对应组件重新渲染

Vue Router的模拟实现(History模式)

回顾代码使用:

// 通过插件引入,Vue.use 方法中可以传入函数或对象,
// 如果传入函数会直接调用这个函数,如果传入对象,Vue.use内部会调用这个对象中的install方法
// VueRouter 是个对象,所以我们需要实现一个install方法
Vue.use(VueRouter)
// 接下来创建实例(所以应该是一个构造函数或者是个类)
// 这里VueRouter 是个类,并且类中有个静态的install
// Vue Router 的参数需要可以传入个路由规则
const router = new VueRouter({
	routes: [
    	{name: 'home', path: '/', component: homeComponent}
    ]
})
// 创建Vue 实例, 注册router对象
new Vue({
	router,
    render: h=> h(App)
}).$mount('#app')

VueRouter -- install

Vue.use 调用install方法时会传递两个参数,一个是Vue构造函数 ,另一个是可选的选项对象

let _Vue = null
class VueRouter {
// 这里我们不需要可选的选项对象
static install(Vue) {
	// 1、判断当前插件是否已经被安装,如果已经被安装了就不需要重复安装
    if(VueRouter.install.installed){
    	return;
    }
    Vue.install.installed = true
    // 2、把Vue 构造函数记录到全局变量中,因为当前install是静态方法,在VueRouter实例方法时需要使用
    _Vue = Vue
    // 3、把创建Vue 实例时候传入的router对象注入到所有的Vue 实例上
    // _Vue.prototype.$router = this.$options.router(但是直接写,的this 是VueRouter)
    // 混入
    _Vue.mixin({
      beforeCreate(){
        ifthis.$options.router){
          _Vue.prototype.$router = this.$options.router
        }
      }
    })
}
}

VueRouter -- 构造函数

class VueRouter {
  constructor(options){
      this.potions = options
      // 路由规则解析成键值对 地址:组件
      this.routeMap = {} 
      // 响应式对象,地址改变,对应改变组件
      this.data = ——Vue.observable({
      current: '/'
      }) 
   }
}

VueRouter createRouterMap

这个方法的作用把路由规则转换成键值对的形式,在地址发生变化时,可以很容易找到组件

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

VueRouter router-link

先看使用方式如下:

<router-link to="/">Home</router-link>

实现方式如下:

initComponents(Vue){
// 创建组件
  Vue.component('router-link', {
    props: {
    to: String
    },
    template: '<a :href="to"><slot></slot></a>' // 创建插槽
  })
}

调用如下

// 添加一个init 方法
init(){
  this.createRouteMap()
	this.initComponents(_Vue)
    
}
并在install 的可访问到的实例中访问他
static install(Vue){
	_Vue.mixin({
      beforeCreate(){
        ifthis.$options.router){
          _Vue.prototype.$router = this.$options.router
          this.$options.router.init()
        }
      }
    })
}

Vue 构建版本

  • 运行时版: 不支持template模板, 需要打包时提前编译(使用render 函数创建虚拟dom ,并将它渲染到试图)
  • 完整版: 包含运行时和编译器(作用是运行时把模板转换成render函数),体积比运行时版大10K左右

vue cli 默认使用的是运行时版本的vue , 所以上面的代码编写方式会报错(单文件组件中也会写template,但是不会报错的原因是,会将其预编译成render 函数)

解决方法:

方法一、使用完整版的Vue, 将vue cli 切换到完整版本

\\ 在vue.config.js
module.exports = {
	runtimeCompiler: true // 开启编译器
}

方法二、还是使用运行时的Vue, 将上述代码中template改成render 函数

initComponents(Vue){
// 创建组件
  Vue.component('router-link', {
    props: {
    to: String
    },
    render (h){
    	return h('a', {
        	attrs: {
            	href: this.to
            }
        },[this.$slot.default]}
    }
    // template: '<a :href="to"><slot></slot></a>' // 创建插槽
  })
}

VueRouter router-view

initComponents(Vue){
...router-link
// 创建组件
 const self = this;
  Vue.component('router-view', {
    props: {
    to: String
    },
    render (h){
    	// 当前路由地址self.data.current 
        const component= self.routeMap[self.data.current]// self.data 是响应式对象,因此路径改变时会引发组件的改变
        // 也就是如果发生组件不改变的bug,有可能时这里的这个对象已经被改变不是响应式了
    	return h(component}
    }
    // template: '<a :href="to"><slot></slot></a>' // 创建插槽
  })
}

pushState

上述代码直接to 是会刷新浏览器的,但是我们不需要刷新浏览器,因此需要用到pushSate, 修改router-link

initComponents(Vue){
// 创建组件
  Vue.component('router-link', {
    props: {
    to: String
    },
    render (h){
    	return h('a', {
        	attrs: {
            	href: this.to
            },
            on: {
            click:this.clickHandler
            }
        },[this.$slot.default]}
    },
    methods: {
    	clickHandler(e){
        // 三个参数(data, title, )第一个参数是之后调用popState 时传递给popState 的事件参数
        // title 网页标题
        // 超链接跳转的地址
        	history.pushState({}, '', this.to) 
            this.$router.data.current = this.to // 响应式对象,会重新加载对应的组件
            e.preventDefault()
        }
    }
    // template: '<a :href="to"><slot></slot></a>' // 创建插槽
  })
}

vueRouter 回退 popState(Hash 模式监听hashChange)

发现目前点击前进和后退的时候,地址栏改变了,但是组件的内容并没有改变,因为没有代码触发current 改变

window.addEventListener('popstate', ()=>{
 	this.data.current = window.location.pathname
})