VUE全家桶之vue-router原理

一、前提思考

我们如何给vue写插件,router其实也是一个插件,一个全局插件。相信大家对于vue.use(xxx)很眼熟了,这个时候让我们来看vue-router是如何使用的吧,最后自己写一个router替换掉vue-router。


vue-router源码地址:https://github.com/vuejs/vue-router

1.1)搭建项目

我们使用vue-cli脚手架搭建项目,你可以在自定义中勾选vue-router,或者自己下载yarn add vue-router

vue create my-vue-router
yarn add vue-router / npm install vue-router

1.2)vue-router使用方式

如果是自定义勾选router后的项目,我们会发现vue-router已经引入项目中使用了,我们来看看它的使用方式。

  • 引入vue-router插件
// 在main只是一个文件的应用,不是插件的引用
import router from './router'
// 真正的引用在router/index.js 并思考use方法,做了什么事情
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
  • 创建router插件
const router = new VueRouter({
  mode: 'xxx',
  base: process.env.BASE_URL,
  routes
})

export default router
  • 挂载router插件
import router from './router'
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
  • 使用router插件跳转路由并渲染dom
<router-link to='xiaojuzi'></router-link>
<router-view></router-view>

this.$router.push('xx')

1.3)vue-router的功能分析

  1. 实现spa单页面应用的路由更新且不刷新页面【我们要实现hash路由和history路由】
  2. 一个路由地址对应一个页面的渲染地址【我们要实现监听和渲染页面,时时render页面】

1.4)写vue插件的思想

1. 实现一个router功能类【你需要完成的功能】

  • 处理路由的类型
  • 监听router的变化
  • 响应router的事件

2. 实现一个初始化插件的方法【你需要完成的功能】

  • $ruoter的组册
  • 完成两个全局组件,即router-view和router-link

二、手写自己的router【实现最简单的router功能】

2.1)创建my-vue-router.js

// 我们在初始化的时候,给我们申明的Vue赋值,这个时候在vue.use使用的时候,
// 我们就可以在自己的my-vue-router.js中拿到vue的构造函数
// 目前只是hash功能/且只支持一层router
let Vue;

class MyRouter {
    constructor(options) {
        this.$options = options

        // 把current作为响应式数据
        // 将来发生变化,router-view的render函数能够再次执行
        const initial = window.location.hash.slice(1) || "/"
        Vue.util.defineReactive(this, 'current', initial)

        // 监听hash变化
        window.addEventListener("hashchange", () => {
            console.log(this.current)
            this.current = window.location.hash.slice(1)
        })
    }
}

// 参数是Vue.use调用时传入的,这里就可以拿到vue咯,保存你的vue吧
MyRouter.install = function (_Vue) {
    Vue = _Vue

    // 1.挂载$router属性
    // this.$router.push()
    // 我们在use的时候,实际上这个时候new vue实例还没有开始执行,我们无法拿到vue的run-time的结果
    // 这个时候我就借用Vue混入的思想,并借用生命周期做到延时效果
    // 最后我们在全局挂载$router
    Vue.mixin({
        beforeCreate () {
            // 次钩子在每个组件创建实例时都会调用
            // 根实例才有该选项
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        }
    })

    // 2.注册实现两个组件router-view,router-link
    Vue.component('router-link', {
        // 必须传入to地址
        props: {
            to: {
                type: String,
                required: true,
            },
        },
        // <a href="to">xxx</a>
        // return <a href={'#'+this.to}>{this.$slots.default}</a>
        render (h) {
            return h(
                "a",
                {
                    attrs: {
                        href: "#" + this.to,
                    },
                },
                this.$slots.default
            )
        }
    })
    Vue.component('router-view', {
        render (h) {
            // 获取当前路由对应的组件
            let component = null
            // 查询routes映射表中,是否有这个route地址
            const route = this.$router.$options.routes.find(
                (route) => route.path === this.$router.current
            )
            // 有这个ruoter地址,则返回route对应的组件地址
            if (route) {
                component = route.component
            }
            console.log(this.$router.current, component)
            return h(component)
        }
    })
}

export default MyRouter

2.1)使用自己的my-vue-router

import Vue from 'vue'
// 这里把 import VueRouter from 'vue-router'改成刚刚自己写的my-vue-router.js文件
import VueRouter from './my-vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  mode: 'hash',
  base: process.env.BASE_URL,
  routes
})

export default router

三、撸人家源码去了

3.1) 人家是怎么监听history路由

hash路由大家好理解,可以兼容hashchange事件
上面我们写了一个监听hash变化的简单版router,想想history也是原理一样,监听popstate事件

// 在人家的vue-router的类中的constructor
constructor (router: Router, base: ?string) {
  window.addEventListener('popstate', e => {
    const current = this.current
    this.transitionTo(getLocation(this.base), route => {
      if (expectScroll) {
        handleScroll(router, route, current, true)
      }
    })
  })
}

3.2) 源码vuerouter类

看人家这个类,干了几件事

  • mode是我们传入的路由类型,这个大家都好理解,在这个文件中,主要是区分两种路由模式
  • 第二个主要是给暴露init/push/replace方法,但其实这里只是一个代理,真正的实现根据模式来区分
export default class VueRouter {
  
  mode: string; // 传入的字符串参数,指示history类别
  history: HashHistory | HTML5History | AbstractHistory; // 实际起作用的对象属性,必须是以上三个类的枚举
  fallback: boolean; // 如浏览器不支持,'history'模式需回滚为'hash'模式
  
  constructor (options: RouterOptions = {}) {
    
    let mode = options.mode || 'hash' // 默认为'hash'模式
    this.fallback = mode === 'history' && !supportsPushState // 通过supportsPushState判断浏览器是否支持'history'模式
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract' // 不在浏览器环境下运行需强制为'abstract'模式
    }
    this.mode = mode

    // 根据mode确定history实际的类并实例化
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

  init (app: any /* Vue component instance */) {
    
    const history = this.history

    // 根据history的类别执行相应的初始化操作和监听
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }

  // VueRouter类暴露的以下方法实际是调用具体history对象的方法
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.push(location, onComplete, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.replace(location, onComplete, onAbort)
  }
}

3.4)源码install方法【实在是妙啊】建议先看我的手写板和注解

  • 熟悉的mixin和钩子beforeCreate
  • 还有借用vue的util中的defineReactive方法,是这个对象变成响应式对象
  • 最后挂载两个全局组件
import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

3.4)源码history

/* @flow */

import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { START } from '../util/route'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'

export class HTML5History extends History {
  _startLocation: string

  constructor (router: Router, base: ?string) {
    super(router, base)

    this._startLocation = getLocation(this.base)
  }

  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current

      // Avoiding first `popstate` event dispatched in some browsers but first
      // history route not updated since async guard at the same time.
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {
        return
      }

      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    }
    window.addEventListener('popstate', handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }

  go (n: number) {
    window.history.go(n)
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      replaceState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  ensureURL (push?: boolean) {
    if (getLocation(this.base) !== this.current.fullPath) {
      const current = cleanPath(this.base + this.current.fullPath)
      push ? pushState(current) : replaceState(current)
    }
  }

  getCurrentLocation (): string {
    return getLocation(this.base)
  }
}

export function getLocation (base: string): string {
  let path = window.location.pathname
  if (base && path.toLowerCase().indexOf(base.toLowerCase()) === 0) {
    path = path.slice(base.length)
  }
  return (path || '/') + window.location.search + window.location.hash
}

3.5)源码hash

/* @flow */

import type Router from '../index'
import { History } from './base'
import { cleanPath } from '../util/path'
import { getLocation } from './html5'
import { setupScroll, handleScroll } from '../util/scroll'
import { pushState, replaceState, supportsPushState } from '../util/push-state'

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }

  // this is delayed until the app mounts
  // to avoid the hashchange listener being fired too early
  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        replaceHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  go (n: number) {
    window.history.go(n)
  }

  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current)
    }
  }

  getCurrentLocation () {
    return getHash()
  }
}

function checkFallback (base) {
  const location = getLocation(base)
  if (!/^\/#/.test(location)) {
    window.location.replace(cleanPath(base + '/#' + location))
    return true
  }
}

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === '/') {
    return true
  }
  replaceHash('/' + path)
  return false
}

export function getHash (): string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  let href = window.location.href
  const index = href.indexOf('#')
  // empty path
  if (index < 0) return ''

  href = href.slice(index + 1)

  return href
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

四、嗯,有些地方我也没看太懂,自己动手实践一下,写下你自己的router吧

Last modification:January 31st, 2021 at 11:13 pm
如果和我讨论问题,请加好友qq/wx