vue-router 源码实现

193 阅读2分钟

1.路由模式

vue-router 的路由模式有三种: hash、history、abstract;

  • hash:浏览器环境,使用 URL hash 值来做路由;支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
  • history:依赖 HTML5 History API 和 服务器配置;
  • abstract:支持所有 javaScript 运行环境,如 node.js 服务器;如果发现没有浏览器的 API,路由会自动强制进入这个模式; 源码逻辑
switch (mode) {
    case  'history' :
       this.historynew HTML5HIstory(this, option.base)
       break
    case   'hash' :
       this.historynew HashHistory(this, option.base,  this.fallback)
       break
    case 'abstract' :
       this.historynew AbstractHistory(this, options.base)
       break
    default :
       if (process.env.NODE_ENV !== 'production') {
           assert(false`invalid mode : ${mode}`)
       }
}        

2.如何使用

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

// 2.声明一个路由表
  const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue')
  }
]

// 3.创建一个Router实例
const router = new VueRouter({
  routes
})

export default router


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

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

//views/About.vue
<template>
  <div class="about">
    <h1>This is an about page</h1>
    <router-view></router-view>
  </div>
</template>

//views/Home.vue
<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>


//App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>


3.代码实现

  1. 定义插件(有install方法) 和 类
  2. router-link和router-view组件标签
  3. 实现监听url变化#hash响应,这里只实现hash模式
  4. 实现路由嵌套

1. 定义插件 和 VueRouter类

//1 定义一个可以实例化的类 
class VueRouter {

}
// 定义插件 需要有一个install的静态方法
VueRouter.install = function(Vue) {

}
export default VueRouter

2. 定义两个组件标签 router-link和router-view

let _Vue //记录一个全局的vue对象 通过install时候传入,有效分离router组件与vue的关系

VueRouter.install = function(Vue) {
  // 引用Vue构造函数,在上面VueRouter中使用
  _Vue = Vue

  // 1.挂载$router
  Vue.mixin({
    beforeCreate() {
      // 此处this指的是组件实例
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
      }
    }
  })

  // 2.定义两个全局组件router-link,router-view
  Vue.component('router-link', {
    // template: '<a>'
    props: {
      to: {
        type: String,
        require: true
      },
    },
    render(h) {
      // <router-link to="/about">
      // <a href="#/about">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) {
      // 找到当前url对应的组件
      const {routeMap, current} = this.$router
      const component = routeMap[current] ? routeMap[current].component : null
      // 渲染传入组件
      return h(component)
    }
  })
}

3. 实现监听url变化#hash响应

通过变量值+vue实现界面响应式


class VueRouter {
  // 选项:routes - 路由表
  constructor(options) {
    this.$options = options
    // 缓存path和route映射关系
    this.routeMap = {}
    this.$options.routes.forEach(
      route => {
        this.routeMap[route.path] = route
      })
    // 需要定义一个响应式的current属性
    const initial = window.location.hash.slice(1) || '/'
    _Vue.util.defineReactive(this, 'current', initial)
    // 监控url变化
    window.addEventListener('hashchange', this.onHashChange.bind(this))
    //如果要实现history模式 ,可以监听popstate实现
  }

  onHashChange() {
    // 只要#后面部分
    this.current = window.location.hash.slice(1)
    console.log(this.current);
  }
}

4. 实现路由嵌套


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

Vue.use(VueRouter)

const routes = [
    {
        path : '/',
        name : "Home",
        component : Home
    },
    {
        path : '/about',
        name : "About",
        component : () => import('../views/About.vue'),
        children : [
            {
                path : '/about/info',
                name : "Info",
                component : Info
            },
        ]
    },
]
const router = new VueRouter(routes)
export default router

//新增views/Info.vue
<template>
  <div class="info">
    <h1>This is an about/info page</h1>
  </div>
</template>


//独立组件文件 router-link.js 和 router-view.js
//router-link.js
export default {
    props: {
        to: String
    },
    render(h){
        return h("a",{
            attrs: {
                href : "#" + this.to
            }
        },
         this.$slots.default
        ) 
    }
}
//router-view.js

export default {
  render(h){
    //新增属性 isRouterView  用于标记自己是isRouterView,注意是父组件 先设置isRouterView=true,再到子组件
    //用于子组件遍历向上遍历是是判断父级是否为isRouterView
      this.$vnode.data.isRouterView = true
     let depth = 0 //默认从根往上找,如 /About/Info/Detail,那么就是从 Detail -> Info -> About,结果depth = 2
      let parent = this.$parent 
      while (parent) {
        const vnodeData = parent.$vnode && parent.$vnode.data
        if(vnodeData) {
          if(vnodeData.isRouterView) {
            depth++
          }
        }
          parent = parent.$parent 
      }
      let component = null 
      console.log("matchList",this.$router.matchList , "depth" ,depth)
      const route = this.$router.matchList[depth]
      if (route) {
        component = route.component
      }
      return h(component)
  }
}

//vue-router.js
import Vue from 'vue'
import RouterLink from './router-link'
import RouterView from './router-view'

let _Vue 
class VueRouter {
    constructor(options) {
        this.current = ''
        this.$options = options
        this.routeMap = {}
        this.$options .forEach(obj => {
            this.routeMap[obj.path] = obj
        });
        this.current = window.location.hash.slice(1) || '/'
        this.matchList = []//响应改成matchList
        Vue.util.defineReactive(this,'matchList',[])
        this.match()
        this.onHashChange = this.onHashChange.bind(this)
        window.addEventListener('hashchange',this.onHashChange)

    }   
    onHashChange() {
        this.current = window.location.hash.slice(1)
        this.matchList = []
        this.match()
    }
    //约束 嵌套router-view
    match(routes) {
        routes = routes || this.$options
        for (const route of routes) { //遍历的是第一层 
            if (route.path === '/' && this.current === '/') { //判断如果是根 则直接返回
                this.matchList.push(route)
                return
            }
            //如果当前是 /about , 
            if (route.path !== '/' && this.current.indexOf(route.path) != -1) {
                this.matchList.push(route) //添加一个/about
                if(route.children) {
                    this.match(route.children) //再递归 添加 children 里的 /about/info 
                }
            }
        }
    }
}

VueRouter.install = function (Vue) {
    _Vue = Vue
    Vue.mixin({
        beforeCreate(){
            if(this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        }
    })
    Vue.component("router-link", RouterLink)
    Vue.component("router-view", RouterView)
}
 
export default VueRouter


4.测试代码

http://localhost:8080/#/about/info

image.png

  • matchList 为解析的vue的list集合
  • depth 为索引

image.png image.png

  1. 第一个 router-view 是在 App.vue,解析的时候在 matchList 长度2 , depth = 0 ,matchList[0]为about元素,所以解析出地址为 http://localhost:8080/#/about/ ,把内容挂载到App.vue上。
  2. 第二个 router-view 是在 About.vue,解析的时候在 matchList 长度2 , depth = 1 ,matchList[1]为info元素,所以解析出地址为 http://localhost:8080/#/about/info ,把内容挂载到About.vue上。

源码地址

gitee.com/mjsong/my-v…