阅读 236

10 分钟教你手写 Vue-Router

Vue-Router 原理

Hash 模式

  • URL 中 # 后面的内容作为路径地址

  • 监听 hashchange 事件

  • 根据当前路由地址找到对应组件重新渲染

History 模式

  • 通过 history.pushState() 方法改变地址栏

  • 监听 popstate 事件

  • 根据当前路由地址找到对应组件重新渲染

手写 Router

下列方法皆为拆解方法,最终会重组在一起

分析

回顾核心代码

Vue.use(VueRouter) // Vue.use 内部传入对象时,会调用对象的 install 方法 // 创建路由对象 const router = new VueRouter({     routes: [         { name: 'home', path: '/', component: homeComponent }     ] }) // 创建 Vue 实例,注册 router 对象 new Vue({     router,     render: h => h(App) }).$mount('#app') 复制代码

  • Vue.use 内部传入对象时,会调用对象的 install 方法,所以我们就来先处理一下 install 方法

install 方法

  • install 接收两个参数

    • vue 的构造函数

    • 可选的选项对象(我们这里没用到,所以不传递)

  • 方法内部分为 3 步

    1. 如果插件已经安装,直接返回

    2. 把 vue 的构造函数记录到全局变量中,因为将来我们要在 VueRouter 的实例方法中使用 vue 的构造函数

    3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上

let _Vue = null export default class VueRouter {   // 传入两个参数,一个是 vue 的构造函数,第二个是 可选的 选项对象   static install (Vue) {     // 1. 如果插件已经安装直接返回     if (VueRouter.install.installed && _vue === true) return     VueRouter.install.installed = true // 表示插件已安装     // 2. 把 vue 的构造函数记录到全局变量中,因为将来我们要在 VueRouter 的实例方法中使用 vue 的构造函数     _Vue = Vue     // 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上     Vue.mixin({       beforeCreate () {         if (this.$options.router) {           _Vue.prototype.$router = this.$options.router         }       }     })   } } 复制代码

constructor

再次我们不需要过多的操作,只需要声明 3 个属性

  1. this.options = options 记录构造函数中传入的选项 options

  2. this.routeMap = {} 把 options 中传入的 routes 也就是路由规则解析出来,键值对的形式,键---路由地址,值---路由组件

  3. this.data = _Vue.observable({ current: '/' })}     observable 创建响应式对象,对象中存储当前路由地址,默认是 /

  constructor (options) {     // 记录构造函数中传入的选项 options     this.options = options     // 把 options 中传入的 routes 也就是路由规则解析出来,键值对的形式,键---路由地址,值---路由组件     this.routeMap = {}     // data 是一个响应式对象,因为 data 中存储的是当前路由地址,路由变化时要自动加载组件     this.data = _Vue.observable({ // observable 创建响应式对象       current: '/' // 存储当前路由地址,默认是 /     })   } 复制代码

createRouteMap

把构造函数中传过来的 选项中的 routes 也就是路由规则转换为键值对的形式转换到routeMap对象中  键---路由地址,值---路由组件

  createRouteMap () {     // 遍历所有的路由规则,以键值对的形式存储在 routeMap 对象中     this.options.routes.forEach(route => {       this.routeMap[route.path] = route.component     })   } 复制代码

initComponents

该方法用来创建 router-link 与 router-view

router-link

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

注意点
  • 如果我们使用 vue_cli 创建项目并且运行时,使用上述创建方法会出现一些报错,原因在于创建标签时使用了 template

  • 而 vue_cli 使用的 运行时版的 vue,不支持 template 模板,需要打包的时候提前编译

解决方法有两种

  1. 修改 vue.config.js 配置使用完整版的 vue

    完整包含运行时和编译器,体积比运行时版大 10k 左右,程序运行的时候把模板转换成 render 函数

    module.exports = {     // 渲染完整版 vue      runtimeCompiler: true } 复制代码

  2. 使用 render 函数

    运行时版本的 vue 不带编译器,所以不支持组件中的 template 选项,编译器的作用就是把 template 编译成 render 函数,运行时的组件可以直接写 render 函数

    单文件组件时一直使用 template 没写 render 是因为在打包的过程中把单文件的 template 编译成 render 函数了,这叫做预编译

render

  • render 函数接受一个参数,通常叫做 h 作用是帮我们创建虚拟 DOM,h 由 vue 传递

  • h 函数接受 3 个参数

      initComponents (Vue) {     Vue.component('router-link', {       props: {         to: String       },   /*     运行时版本的 vue 不带编译器,所以不支持组件中的 template 选项,编译器的作用就是把 template 编译成 render 函数,运行时的组件可以直接写 render 函数 单文件组件时一直使用 template 没写 render 是因为在打包的过程中把单文件的 template 编译成 render 函数了,这叫做预编译   */       render (h) { // 该函数接收一个参数,通常叫做 h 作用是帮我们创建虚拟 DOM,h 由 vue 传递         return h('a', { // h 函数接受 3 个参数,1. 创建这个元素的选择器。2. 给标签设置一些属性,如果是 DOM 对象的属性,需要添加到 attrs 中。3. 生成的元素的子元素,所以是数组形式           attrs: {             href: this.to           }         }, [this.$slots.default])       }       // template: '<a :href="to"><slot></slot></a>'     })   } 复制代码

    1. 创建这个元素的选择器。

    2. 给标签设置一些属性,如果是 DOM 对象的属性,需要添加到 attrs 中。

    3. 生成的元素的子元素,所以是数组形式

router-view

    const self = this Vue.component('router-view', {       // 获取到当前路由地址对应的路由组件       render (h) {         const component = self.routeMap[self.data.current]         return h(component)       }     }) 复制代码

集合上述方法 检查是否存在问题

  • 因为我们默认只会调用 install 方法,但是我们还有一些其他方法需要调用,还需要一个 init 方法,在 install 方法 被调用后,调用 createRouteMap() 与 initComponents()

  init () {     this.createRouteMap()     this.initComponents(_Vue)   } 复制代码

  • 将上述所有方法整合(为了便于阅读,去掉注释)

    // ../vuerouter/index.js let _Vue = null export default class VueRouter {   static install (Vue) {     if (VueRouter.install.installed && _Vue === true) return     VueRouter.install.installed = true     _Vue = Vue     Vue.mixin({       beforeCreate () {         if (this.$options.router) {           _Vue.prototype.$router = this.$options.router           this.$options.router.init()         }       }     })   }   constructor (options) {     this.options = options     this.routeMap = {}     this.data = _Vue.observable({       current: '/'     })   }   init () {     this.createRouteMap()     this.initComponents(_Vue)   this.initEvent()   }   createRouteMap () {     this.options.routes.forEach(route => {       this.routeMap[route.path] = route.component     })   }   initComponents (Vue) {     Vue.component('router-link', {       props: {         to: String       },   render (h) {         return h('a', {           attrs: {             href: this.to           }         }, [this.$slots.default])       }     })     const self = this     Vue.component('router-view', {       render (h) {         const component = self.routeMap[self.data.current]         return h(component)       }     })   } } // 记得替换 router/index.js 中引入的 VueRouter import Vue from 'vue' // import VueRouter from 'vue-router' import VueRouter from '../vuerouter' import Home from '../views/Home.vue' import Home from '../views/About.vue' Vue.use(VueRouter) const routes = [   {     path: '/',     name: 'Home',     component: Home   },   {     path: '/about',     name: 'About',     component: About   } ] const router = new VueRouter({   routes }) export default router 复制代码

    完善 router-link

        Vue.component('router-link', {       props: {         to: String       },       render (h) {         return h('a', {           attrs: {             href: this.to           },   on: {   click: this.clickHandler   }         }, [this.$slots.default])       },   methods: {   clickHandler (e) {           // 1. 通过 pushState 方法 改变浏览器地址栏,但不给服务器发请求           history.pushState({}, '', this.to) // 该方法接受 3 个参数,1. data。2. title 网页的标题。3. url 地址           this.$router.data.current = this.to           e.preventDefault()   }   }     }) 复制代码

    • 我们需要给 a 标签添加 1 个 方法,这个方法有三个作用

      pushState 方法接受 3 个参数

    • 实际测试后,我们会发现,上述仍然存在一些小问题,也就是我们在点击 router-link 时,会改变路径刷新页面,但是在单页面组件中,我们是不需要刷新页面的,所以我们需要对 router-link 在做一点小调整

    1. 通过 pushState 方法 改变浏览器地址栏,但不给服务器发请求

    2. 将当前路由路径 同步给默认值

    3. 阻止标签默认事件

    1. data。

    2. title 网页的标题。

    3. url 地址。

最终的完善

将上边的 router-link 完善后,我们的 Vue-Router 就实现了,但是还差一个小功能,也就是浏览器左上角的前进后退就失效了,所以我们需要在添加一个小方法来实现最终的完善

  • 其实就是通过 监听 popstate 将当前的路由路径赋值给 current 来达到左上角的前进后退功能**(记得将其添加到 init 方法中)**

  initEvent () {   window.addEventListener('popstate', () => {   this.data.current = window.location.pathname   })   } 复制代码

  • 最终完整版代码(分为两版,有注释的在最后)

let _Vue = null export default class VueRouter {   static install (Vue) {     if (VueRouter.install.installed && _Vue === true) return     VueRouter.install.installed = true     _Vue = Vue     Vue.mixin({       beforeCreate () {         if (this.$options.router) {           _Vue.prototype.$router = this.$options.router           this.$options.router.init()         }       }     })   }   constructor (options) {     this.options = options     this.routeMap = {}     this.data = _Vue.observable({       current: '/'     })   }   init () {     this.createRouteMap()     this.initComponents(_Vue) this.initEvent()   }   createRouteMap () {     this.options.routes.forEach(route => {       this.routeMap[route.path] = route.component     })   }   initComponents (Vue) {     Vue.component('router-link', {       props: {         to: String       },       render (h) {         return h('a', {           attrs: {             href: this.to           },   on: {   click: this.clickHandler   }         }, [this.$slots.default])       },   methods: {   clickHandler (e) {           history.pushState({}, '', this.to)           this.$router.data.current = this.to           e.preventDefault()   }   }     })     const self = this     Vue.component('router-view', {       render (h) {         const component = self.routeMap[self.data.current]         return h(component)       }     })   }   initEvent () {   window.addEventListener('popstate', () => {   this.data.current = window.location.pathname   })   } } 复制代码

let _Vue = null export default class VueRouter {   // 传入两个参数,一个是 vue 的构造函数,第二个是 可选的 选项对象   static install (Vue) {     // 1. 如果插件已经安装直接返回     if (VueRouter.install.installed && _Vue === true) return     VueRouter.install.installed = true // 表示插件已安装     // 2. 把 vue 的构造函数记录到全局变量中,因为将来我们要在 VueRouter 的实例方法中使用 vue 的构造函数     _Vue = Vue     // 3. 把创建 Vue 实例时候传入的 router 对象注入到 Vue 实例上     Vue.mixin({       beforeCreate () {         if (this.$options.router) {           _Vue.prototype.$router = this.$options.router           this.$options.router.init()         }       }     })   }   constructor (options) {     // 记录构造函数中传入的选项 options     this.options = options     // 把 options 中传入的 routes 也就是路由规则解析出来,键值对的形式,键---路由地址,值---路由组件     this.routeMap = {}     // data 是一个响应式对象,因为 data 中存储的是当前路由地址,路由变化时要自动加载组件     this.data = _Vue.observable({ // observable 创建响应式对象       current: '/' // 存储当前路由地址,默认是 /     })   }   init () {     this.createRouteMap()     this.initComponents(_Vue) this.initEvent()   }   // 把构造函数中传过来的 选项中的 routes  也就是路由规则转换为键值对的形式转换到routeMap对象中   键---路由地址,值---路由组件   createRouteMap () {     // 遍历所有的路由规则,以键值对的形式存储在 routeMap 对象中     this.options.routes.forEach(route => {       this.routeMap[route.path] = route.component     })   }   // 创建两个组件 router-link 与 router-view   initComponents (Vue) { // 接收参数,减少与外界联系     Vue.component('router-link', {       props: {         to: String       },   /*     运行时版本的 vue 不带编译器,所以不支持组件中的 template 选项,编译器的作用就是把 template 编译成 render 函数,运行时的组件可以直接写 render 函数 单文件组件时一直使用 template 没写 render 是因为在打包的过程中把单文件的 template 编译成 render 函数了,这叫做预编译   */       render (h) { // 该函数接收一个参数,通常叫做 h 作用是帮我们创建虚拟 DOM,h 由 vue 传递         return h('a', { // h 函数接受 3 个参数,1. 创建这个元素的选择器。2. 给标签设置一些属性,如果是 DOM 对象的属性,需要添加到 attrs 中。3. 生成的元素的子元素,所以是数组形式           attrs: {             href: this.to           },   on: {   click: this.clickHandler   }         }, [this.$slots.default])       },   methods: {   clickHandler (e) {           // 1. 通过 pushState 方法 改变浏览器地址栏,但不给服务器发请求           history.pushState({}, '', this.to) // 该方法接受 3 个参数,1. data。2. title 网页的标题。3. url 地址           this.$router.data.current = this.to           e.preventDefault()   }   }       // template: '<a :href="to"><slot></slot></a>'     })     const self = this     Vue.component('router-view', {       // 获取到当前路由地址对应的路由组件       render (h) {         const component = self.routeMap[self.data.current]         return h(component)       }     })   }   initEvent () {   window.addEventListener('popstate', () => {   this.data.current = window.location.pathname   })   } }


作者:AHXGD
链接:https://juejin.cn/post/7032213292772753416

文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐