新闻资讯

新闻资讯 行业动态

如何实现vue-router?

编辑:008     时间:2020-02-18

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

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基本完成。



郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。

回复列表

相关推荐