手写一个简易版的VueRouter(demo)

662 阅读2分钟

前言 Vue-router,也就是路由,相信大家对它都不会陌生。本篇文章用于记录自己学习VueRouter实现的过程,大家跟我一起来看看吧。 路由的基本使用 VueRouter作为Vue全家桶重要组成部分,基本每个项目都会用它,因为Vue是单页面应用,可以通过路由来实现切换组件,达到切换页面的效果。具体实现分为以下3部分

  • 1、引入vue-router,并使用Vue.use(VueRouter)image.png
  • 2、定义路由数组,并将数组传入VueRouter实例,创建VueRouter实例,并将实例导出image.png
  • 3、将VueRouter实例引入到main.js,并注册到根Vue实例上image.png

实现思路

  • 1、实现一个VueRouter类,该类里具体实现以下功能
      1. 处理路由选项
      1. 监控url变化(本文以hash模式为例)
      1. 响应这个变化
       class VueRouter {
         constructor(options) {
           // 1. 保存路由选项
           this.$options = options
           // 此种写法,current非响应式
           // this.current = window.location.hash.slice(1) || '/'
           // 如何让current成为一个响应式数据?非响应式数据 current改变并不能触发h函数重新渲染组件
           // Vue.set()要求第一个参数必须是一个响应式数据对象
           // Vue内置方法此方法可以给一个对象指定一个响应式属性
           Vue.util.defineReactive(this, 'current', window.location.hash.slice(1) || '/')
           // 2. 监控hash变化
           window.addEventListener('hashchange', () => {
             // hash: #/about
             this.current = window.location.hash.slice(1)
           })
         }
       }
    
  • 2、实现install方法,该方法里具体实现以下功能
      1. 注册$router,让所有的组件实例都可以访问到他
      1. 注册两个全局组件 router-link, router-view
    • Vue.use(VueRouter)执行时,会执行内部的install方法,传入Vue的构造函数,可以对其进行扩展
    • 需要考虑的问题
      • 1). $router挂载时机问题,如果直接使用Vue.prototype.$router=router的话,因为Vue.use(VueRouter)的执行时机早于new VueRouter()new Vue(),所以此时是拿不到router实例的。考虑使用mixinimage.png
            Vue.mixin({
               beforeCreate() { // 组件实例化的时候执行
                 // this 是组件实例 $options 每个组件实例的$options可以得到当前组件的配置信息
                 if (this.$options.router) { // 如果存在说明是根实例
                   // 该行代码需要延迟执行,延迟到router实例和vue实例都创建完毕
                   Vue.prototype.$router = this.$options.router
                  }
               }
             })
        
      1. 注册两个全局组件 router-link, router-view
            // <router-link to="/home">Home</router-link>
            Vue.component("router-link", {
              props: {
                to: {
                  type: String,
                  required: true,
                },
              },
              render(h) {
                // <a href="to">xxx</a>
                // return <a href={'#'+this.to}>{this.$slots.default}</a>
                return h(
                  "a",
                  {
                    attrs: {
                      href: "#" + this.to,
                    },
                  },
                  this.$slots.default // 匿名插槽的所有内容
                );
              },
            });
        
            Vue.component("router-view", {
              render(h) {
                /* 
                  1. 获取当前url的hash部分
                  2. 根据hash部分从路由表中获取对应的组件
                */
                // 获取当前路由对应的组件
                let component = null
                // 当前this就是当前router-view组件的实例 this.$router存在是因为在上面mixin混入的时候 在所有组件身上都挂在了$router
                // this.$router.current当前路由器实例 this.$router.$options.routes是new VueRouter中传递过来的routes映射关系
                const route = this.$router.$options.routes.find(
                  (route) => route.path === this.$router.current
                );
                if (route) {
                  component = route.component
                }
                console.log(this.$router.current, component);
                return h(component);
              },
            })
        

总结 以上就是实现一个简易版VueRouter的过程,因为写的是个demo,暂未考虑嵌套路由等问题。希望大佬们多多指教。 完整代码

    // src/myrouter/myvue-router.js
    
/* 
  自己创建一个router 声明自己的VueRouter
*/
let Vue;
class VueRouter {
  constructor(options) {
    // 1. 保存路由选项
    this.$options = options
    // this.current = window.location.hash.slice(1) || '/'
    // 如何让current成为一个响应式数据  非响应式数据 current改变并不能触发h函数重新渲染组件
    // Vue.set()要求第一个参数必须是一个响应式数据对象
    Vue.util.defineReactive(this, 'current', window.location.hash.slice(1) || '/') // 此方法可以给一个对象指定一个响应式属性
    // 2. 监控hash变化
    window.addEventListener('hashchange', () => {
      // hash: #/about
      this.current = window.location.hash.slice(1)
    })
  }
}
// 参数1 Vue的构造函数 用形参传入进来而不使用import vue导入进来 可以优化打包进程 (写个插件把vue打包进来不合适)
VueRouter.install = function (_Vue) {
  // 传入构造函数 能对其进行扩展
  Vue = _Vue
  // 1. 注册$router 让所有的组件实例都可以访问到他
  // 混入 -- 当你想访问vue实例或组件实例的时候 都需要混入 在混入的时候获取到this 组件实例
  Vue.mixin({
    beforeCreate() { // 组件实例化的时候执行
      // this 是组件实例 $options 每个组件实例的$options可以得到当前组件的配置信息
      if (this.$options.router) {
        // 如果存在说明是根实例
        Vue.prototype.$router = this.$options.router // 该行代码需要延迟执行,延迟到router实例和vue实例都创建完毕
      }
    }
  })
  // 2. 注册两个全局组件 router-link router-view
  // <router-link to="/home">Home</router-link>
  Vue.component("router-link", {
    props: {
      to: {
        type: String,
        required: true,
      },
    },
    render(h) {
      // <a href="to">xxx</a>
      // return <a href={'#'+this.to}>{this.$slots.default}</a>
      return h(
        "a",
        {
          attrs: {
            href: "#" + this.to,
          },
        },
        this.$slots.default // 匿名插槽的所有内容
      );
    },
  });
  Vue.component("router-view", {
    render(h) {
      /* 
        1. 获取当前url的hash部分
        2. 根据hash部分从路由表中获取对应的组件
      */
      // 获取当前路由对应的组件
      let component = null
      // 当前this就是当前router-view组件的实例 this.$router存在是因为在上面mixin混入的时候 在所有组件身上都挂在了$router
      // this.$router.current当前路由器实例 this.$router.$options.routes是new VueRouter中传递过来的routes映射关系
      const route = this.$router.$options.routes.find(
        (route) => route.path === this.$router.current
      );
      if (route) {
        component = route.component
      }
      console.log(this.$router.current, component);
      return h(component);
    },
  });

}
export default VueRouter
// src/myrouter/index.js
import Vue from 'vue'
import VueRouter from './myvue-router'
import Home from '../views/Home.vue'

/* 1.VueRouter是一个插件?
内部做了什么:
   1)实现并声明两个组件router-view  router-link
   2) install: this.$router.push() 
*/
/* 
  vue插件有两种形态 fn object
  Vue.use会调用插件的install(Vue)方法 传递的对象为Vue的构造函数
*/
Vue.use(VueRouter)

const routes = [
  // path和compent存在映射关系
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

// 2.创建实例
// 重点 在上面Vue.use(VueRouter)调用install方法的时候 new VueRouter() 还没有执行 意味着这个VueRouter实例目前还不存在
const router = new VueRouter({
  mode: 'hash',
  base: process.env.BASE_URL,
  routes // 传递给VueRouter
})

export default router

// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './myrouter'

Vue.config.productionTip = false
// 事件总线
Vue.prototype.$bus = new Vue()

// Vue实例化, 在vueRouter实例创建完成后创建Vue实例 把router,store作为选项传入
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')