一文带你实现vue-router

2,859 阅读2分钟

根据vue-router的源码,实现一个自己的vue-router。

写在前面

本文将带你手写一个vue-router,实现router-view,实现hash模式,嵌套路由等主要的功能,history模式建议自行看源码,原理就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面,但是需要后端做配合,没有hash直接上手使用来的方便简单,所以本篇将主要将hash的实现,关于history的有兴趣的可以看这里vue-router本篇完整代码


让我们现在初始化一个项目

vue create my-router cd vuex-test yarn serve

router.js
import Vue from 'vue'
import Router from '../myRouter'
import Home from '../src/components/Home'
import About from '../src/components/About'

Vue.use(Router)

const routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/about',
    component: About,
    children: [
      {
        path: 'a',
        component: {
          render() {
            return <h1>About A</h1>
          }
        }
      },
      {
        path: 'b',
        component: {
          render() {
            return <h1>About B</h1>
          }
        }
      }
    ]
  }
]

const router = new Router({
  routes
})

export default router

基本的嵌套路由,访问'/'渲染Home组件,访问'/about'渲染About组件,访问'/about/a'渲染About组件和子组件About A,访问'/about/b'渲染About组件和子组件About B。嵌套路由'/about/b'一定是匹配到父组件,然后由父组件去渲染子组件的router-view组件。

install方法

install.js

export let _vue
/**
 * 1. 注册全局属性 $route $router
 * 2. 注册全局组件 router-view router-link
 */
export default function install(Vue) {
  _vue = Vue
  Vue.mixin({
    beforeCreate () {
      if(this.$options.router) {
        this._routerRoot = this
        this._router = this.$options.router
      } else {
        this._routerRoot = this.$parent && this.$parent._routerRoot
      }
    }
  })
}

我们知道vue-router的用法是vue.use(router),vue会调用install方法进行初始化。初始化分为俩个步骤,1. 注册全局属性 route router,2. 注册全局组件 router-view router-link,我们这次主要讲router-view。 install只要是判断是否为根组件,只有根组件才会传入router实例,根组件我们将_routerRoot指向根实例,_router指向router实例,这样所有的子组件都能通过$parent._routerRoot拿到_router这个router实例。

数据扁平化

class Router
import createMatcher from './create-matcher'
import install from './install'

export default class Router {
  constructor(options) {
    /**
     * 将用户的数据扁平化
     * [
     *  {
     *    path: '/ss',
     *    component: SSS
     *  }
     * ]
     * => {'/ss': SSS, '/sss/aa': a}
     * 
     * matcher会有俩个方法
     * 1. match 用来匹配路径和组件
     * 2. addRoutes 用来动态的添加组件
     */

    this.matcher = createMatcher(options.routes || [])
  }
  init(app) {  // main vue
    const setupHashLinster = () => {
      history.setupHashLinstener()
    }

    history.transitionTo(
      // 首次进入的时候跳转到对应的hash
      // 回调用来监听hash的改变
      history.getCurrentLocation(),
      setupHashLinster
    )
  }
}

Router.install = install

数据扁平化就是将我们的用户传进来的routes给拆成我们想要的数据结构。vue中create-matcher.js单独用来做这个事情。

createMatcher

create-matcher.js

import createRouteMap from './create-route-map'

export default function createMatcher(routes) {
  /**
   * pathList => 路径的一个关系array [/sss, /sss/s, /sss/b]
   * pathMap => 路径和组件的关系map {/sss: 'ss', ....}
   */
  let { pathList, pathMap } = createRouteMap(routes)
}

create-route-map.js
export default function createRouteMap(routes, oldPathList, oldPathMap) {
  let pathList = oldPathList || []
  let pathMap = oldPathMap || Object.create(null)
  routes.forEach((route) => {
    addRouteRecord(route, pathList, pathMap)
  })

  return {
    pathList,
    pathMap
  }
}

function addRouteRecord(route, pathList, pathMap, parent) {
  let path = parent ? `${parent.path}/${route.path}` : route.path
  let record = {
    path,
    component: route.component,
    parent
  }
  if (!pathMap[path]) {
    pathList.push(path)
    pathMap[path] = record
  }
  if (route.children) {
    route.children.forEach((child) => {
      addRouteRecord(child, pathList, pathMap, route)
    })
  }
}

createRouteMap创建pathList, pathMap对应关系,将数据扁平话。使用递归addRouteRecord将'/about/a'转化成

{
	'/about': {
		path: '/about', 
		component: About,
		parent: null
	},
	'/about/a': {
		path, 
		component: About A,
		parent: About // 指父路由
	}
}

match方法的作用

create-matcher.js
import { createRoute } from '../history/base'
  /**
   * 用来匹配路径
   */
 function match(location) {
   /**
    * 更具路径匹配组件并不能直接渲染组件,因该找到所有要匹配的项
    * path: 'about/a' => [about, aboutA]
    * 只有将父子组件都渲染才能完成工作。
    */
   let record = pathMap[location]
   let local = {
     path: location
   }
   if (record) {
     return createRoute(record, local)
   }
   return createRoute(null, local)
 }
 /**
 * 动态路由,可以动态添加路由,并将添加的路由放到路由映射表中
 */
 function addRoutes(routes) {
   createRouteMap(routes, pathList, pathMap)
 }

 return {
   match,
   addRoutes
 };

base.js
export function createRoute(record, location) {
  let res = []
  if (record) {
    while(record) {
      res.unshift(record)
      record = record.parent
    }
  }
  return {
    ...location,
    matched: res
  }
}

match是用来将当前的路径跟我们用户初始化参数做匹配的,并将需要渲染的组件给返回,createRoute将传入的匹配数据和当前的地址拼接返回{path: '/about/a', matched: [About, AboutA]}。

History

vue-router有三个模式,hash,history,abstract,每个模式都有对url的操作,共有的方法放在class Base中,自己独有的就放在自己的类中。History要实现路由的改变的监听,并将改变后的数据match出对应的组件。

export default class Base {
  constructor(router) {
    this.router = router
    /**
     * 默认匹配项,后续会根据路由改变而替换
     * 保存匹配到的组件
     */
    this.current = createRoute(null, {
      path: '/'
    })
  }
  /**
   * location 要跳转的路径
   * onComplete 跳转完成之后的回调
   */
  transitionTo(location, onComplete) {
    /**
     * 去匹配当前hash的组件
     */
    let route = this.router.match(location)
    /**
     * 匹配完成,将current给修改掉
     * 相同路径就不进行跳转了
     */
    if(this.current.path === location && route.matched.length === this.current.matched.length) return
    /**
     * 有了当前的current,我们的vue各个组件该怎样访问
     */
    this.updateRoute(route)
    onComplete && onComplete()
  }

  updateRoute(route) {
    this.current = route
    this.cb && this.cb(route)
  }

  linsten(cb) {
    this.cb = cb
  }
}

我们用this.current保存匹配到的组件,并提供一个方法transitionTo,当url改变时去匹配改变以后的组件并将this.current给修改掉。现在我们需要将class Hash和Base联起来。

实现Hash

hash.js
import Base from './base'

const getHash = () => {
  return window.location.hash.slice(1)
}

const ensureSlash = () => {
  if (window.location.hash) return
  window.location.hash = '/'
}

export default class Hash extends Base {
  constructor(router) {
    super(router)
    // 确保hash是有#/的
    ensureSlash()
  }

  getCurrentLocation() {
    return getHash()
  }

  setupHashLinstener() {
    window.addEventListener('hashchange', () => {
      this.transitionTo(getHash())
    })
  }
}
}

setupHashLinstener实际上就是transitionTo(location, onComplete) 的第二个参数,在首次初始化路由路由为/,并完成hashchange的监听,当hash改变在执行transitionTo把当前的hash去match出最新的matched组件,然后修改this.current。

路由是响应式的

上面我们的所有工作都是围绕current来做的,当url改变我们去match最新的组件给current。当current改变的时候就自动更新组件。当current改变我们要将_route这个属性改变,所以就用到base的linsten方法。

install.js
// 调用router的init
this._router.init(this) this指的是根实例
// 怎样让this.current变成响应式的
// Vue.util.defineReactive = vue.set()
Vue.util.defineReactive(this, '_route', this._router.history.current)

class Router
init(app) {  // 根实例
  const history = this.history
  const setupHashLinster = () => {
    history.setupHashLinstener()
  }

  history.transitionTo(
    // 首次进入的时候跳转到对应的hash
    // 回调用来监听hash的改变
    history.getCurrentLocation(),
    setupHashLinster
  )

  history.linsten((route) => {
	// current改变会执行这个回调,修改_route
    app._route = route
  })
}

添加全局的属性

我们需要个每个vue组件添加 route属性和router属性。

  install.js
  /**
   * 怎么让所有的组件都能访问到current
   */
  Object.defineProperty(Vue.prototype, '$route', {
    get() {
      return this._routerRoot._route
    }
  })
  /**
   * 怎么让所有的组件都能访问到router实例
   */
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot._router
    }
  })
}

添加全局的组件

routerView函数式组件,render第二个参数是context可以拿到当前组件的状态。我们拿到$route就能拿到当前url对应的组件。 这里解释一下while循环。当我们渲染'/about/a'时,matched=[About, aboutA]俩组件,第一次渲染的是About组件,depth = 0,当渲染aboutA时,parent = About满足条件depth++,然后渲染aboutA组件。

install.js
 /**
  * 注册全局组件
  */
 Vue.component('router-view', routerView)
 
router-view.js
export default {
  functional: true,
  render(h, {parent, data}) {
    let route = parent.$route
    let matched = route.matched
    // 组件标示,表示是个routerView组件
    data.routerView = true
    let depth = 0
    while(parent) {
      if(parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      parent = parent.$parent
    }
    let record = matched[depth]
    if(record) {
      let component = record.component
      return h(component, data)
    } else {
      return h()
    }
  }
}

vue-router基本完成,谢谢你能看到这里。