最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue-Router实现

    正文概述 掘金(式溪)   2021-01-18   572

    看完拉勾前端训练营关于Vue-Router的实现,干货满满,但Vue-Router的实现实在是绕,所以做一下笔记,确认以及加深自己的了解。进了拉勾前端训练营两个多月,收获还是挺多的,群里不少大牛,还有美女班主任,导师及时回答学员的疑问,幽默风趣,真是群里一席谈,胜读四年本科(literally true,四年本科的课程真的水=_=)。

    实现的功能

    实现前,看一下实现的功能:

    1. 基本路由功能
    2. 子路由功能
    3. History及Hash功能

    创建一个项目。首先肯定是要创建Vue Router的类,在根目录下创建index.js文件:

    export default class VueRouter {
        constructor (option) {
            this._routes = options.routes || []
        }
    
        init () {}
    }
    

    我们平时创建路由实例时,会传入一个对象,像这样:

    const router = new VueRouter({
      routes
    })
    

    所以构造函数应该要有一个对象,如果里面有路由routes,赋值给this._routes,否则给它一个空数组。options里当然有其他属性,但先不管,之后再实现。 还有一个init方法,用来初始化设定。

    install

    由于Vue Router是插件,要想使用它,必须通过Vue.use方法。该方法会判定传入的参数是对象还函数,如果是对象,则调用里面的install方法,函数的话则直接调用。 Vue Router是一个对象,所以要有install方法。实现install之前,看一下Vue.use的源码,这样可以更好理解怎样实现install:

    export function initUse (Vue: GlobalAPI) {
    
      Vue.use = function (plugin: Function | Object) {
        const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
        if (installedPlugins.indexOf(plugin) > -1) {
          return this
        }
    
        const args = toArray(arguments, 1)
        args.unshift(this)
        if (typeof plugin.install === 'function') {
          plugin.install.apply(plugin, args)  
        } else if (typeof plugin === 'function') {
          plugin.apply(null, args)
        }
        installedPlugins.push(plugin)
        return this
      }
    }
    

    首先Vue.use会先判定Vue有没有一个属性叫_installedPlugins,有则引用,没有就为Vue添加属性_installedPlugins,它是一个空数组,再去引用它。_installedPlugins是记录安装过的插件。接下来判定_installedPlugins里有没有传入的插件,有则不用安装。 把传入的参数从第二个开始,变成数组,把Vue放入数组首位。如果插件是对象,则调用它的install方法,插件方法里的上下文this依然是它自身,传入刚才变成数组的参数。函数的话,不用考虑上下文,直接调用。最后记录该插件是安装过的。

    现在简单把install方法实现,在根目录下新建install.js:

    export let _Vue = null
    export default function install (Vue) {
      _Vue = Vue
      _Vue.mixin({
        beforeCreate () {
          if (this.$options.router) {
            this._router = this.$options.router
            this._routerRoot = this
            // 初始化 router 对象
            this._router.init(this)
          } else {
            this._routerRoot = this.$parent && this.$parent._routerRoot
          }
        }
      })
    

    全局变量_Vue是为了方便其他Vue Router模块的引用,不然的话其他模式需要引入Vue,比较麻烦。mixin是把Vue中某些功能抽取出来,方便在不同地方複用,这里的用法是全局挂载鈎子函数。

    先判断是否为根实例,如果是根实例,会有路由传入,所以会$options.router存在。根实例的话则添加两个私有属性,其中_routerRoot是为了方便根实例以下的组件引用,然后初始化router。如果是根实例下的组件,去找一下有没有父组件,有就引用它的_routerRoot,这样可以通过_routerRoot.router来引用路由。

    挂载函数基本完成。当我们使用Vue Router,还有两个组件挂载:Router Link和Router View。在根目录下创建文件夹components,创建文件link.js和view.js。先把Router Link实现:

    export default {
      name: 'RouterLink',
      props: {
        to: {
          type: String,
          required: true
        }
      },
      render (h) {
        return h('a', { attrs: { href: '#' + this.to } }, [this.$slots.default])
      }
    }
    

    RouterLink接收一个参数to,类型是字符串。这里不使用template,是因为运行版本的vue没有编译器,把模板转为渲染函数,要直接用渲染函数。 简单讲一下渲染函数的用法,第一个参数是标签类型,第二个是标签的属性,第三是内容。详细可以看vue文档。 我们要实现的其实是<a :href="{{ '#' + this.to }}"><slot name="default"></slot></a>。所以第一个参数是a,第二个它的连接,第三个之所以要用数组,是因为标签的内容是一个slot标签节点,子节点要用数组包起来。 至于RouterView,现在不知道它的实现,大概写一下:

    export default {
      name: 'RouterView',
      render (h) {
        return h () 
      }
    }
    

    在install里把两个组件注册:

    import Link from './components/link'
    import View from './components/view'
    export default function install (Vue) {
       ...
      _Vue.component(Link.name, Link)
      _Vue.component(View.name, View)
    }
    

    createMatcher

    接下来要创建create-matcher,它是用来生成匹配器,主要返回两个方法:match和addRoutes。前者是匹配输入路径,获取路由表相关资料,后者是手动添加路由规则到路由表。这两个方法都是要依赖路由表,所以我们还要实现路由表生成器:create-router-map,它接收路由规则,返回一个路由表,它是对象,里面有两个属性,一个是pathList,它是一个数组,存有所有路由表的路径,另一个是pathMap,是一个字典,键是路径,而值的路径相应的资料。 在项目根目录下创建create-router-map.js:

    export default function createRouteMap (routes) {
    
      // 存储所有的路由地址
      const pathList = []
      // 路由表,路径和组件的相关信息
      const pathMap = {}
    
      return {
        pathList,
        pathMap
      }
    }
    

    我们需要遍历路由规则,在这过程中做两件事:

    1. 把所有路径存入pathList
    2. 把路由和资料对应关係放入pathMap

    这里的难点是有子路由,所以要用递归,但现在先不要考虑这问题,简单把功能实现:

    function addRouteRecord (route, pathList, pathMap, parentRecord) {
      const path = route.path
      const record = {
        path: path,
        component: route.component,
        parentRecord: parentRecord
        // ...
      }
    
      // 判断当前路径,是否已经存储在路由表中了
      if (!pathMap[path]) {
        pathList.push(path)
        pathMap[path] = record
      }
    }
    

    现在考虑一下子路由的问题。首先要先有判定路由是否有子路由,有的话遍历子路由,递归处理,还要考虑路径名称问题,如果是子路由,path应该是父子路径合并,所以这里要判定是否存有父路由。

    function addRouteRecord (route, pathList, pathMap, parentRecord) {
      const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path
      const record = {
        path: path,
        component: route.component,
        parentRecord: parentRecord
        // ...
      }
    
      // 判断当前路径,是否已经存储在路由表中了
      if (!pathMap[path]) {
        pathList.push(path)
        pathMap[path] = record
      }
    
      // 判断当前的route是否有子路由
      if (route.children) {
        route.children.forEach(childRoute => {
          addRouteRecord(childRoute, pathList, pathMap, route)
        })
      }
    }
    

    如果有传入父路由资料,path是父子路径合并。

    最后把addRouteRecord添加到createRouteMap:

    export default function createRouteMap (routes) {
      // 存储所有的路由地址
      const pathList = []
      // 路由表,路径和组件的相关信息
      const pathMap = {}
    
      // 遍历所有的路由规则 routes
      routes.forEach(route => {
        addRouteRecord(route, pathList, pathMap)
      })
    
      return {
        pathList,
        pathMap
      }
    }
    

    createRouteMap实现了,可以把create-matcher的路由表创建和addRoute实现:

    import createRouteMap from './create-route-map'
    
    export default function createMatcher (routes) {
      const { pathList, pathMap } = createRouteMap(routes)
    
      function addRoutes (routes) {
        createRouteMap(routes, pathList, pathMap)
      }
      return {
        match,
        addRoutes
      }
    }
    

    最后要实现match了,它接收一个路径,然后返回路径相关资料,相关资料不仅仅是它自身的,还有它的父路径的资料。这里先实现一个工具类函数,它是专门创建路由的,就是返回路径以及它的相关资料。创建util/route.js:

    export default function createRoute (record, path) {
      // 创建路由数据对象
      // route ==> { matched, path }  matched ==> [record1, record2]
      const matched = []
    
      while (record) {
        matched.unshift(record)
    
        record = record.parentRecord
      }
    
      return {
        matched,
        path
      }
    

    其实功能很简单,就是不断获取上一级的资料,放进数组首位。配上createRoute,match基本就实现了:

    import createRoute from './util/route'
    
      function match (path) {
        const record = pathMap[path]
        if (record) {
          // 创建路由数据对象
          // route ==> { matched, path }  matched ==> [record1, record2]
          return createRoute(record, path)
        }
        return createRoute(null, path)
      }
    

    在VueRouter的构造函数里把matcher加上:

    import createMatcher from './create-matcher'
    
    export default class VueRouter {
      constructor (options) {
        this._routes = options.routes || []
        this.matcher = createMatcher(this._routes)
    ...
    

    History历史管理

    matcher做好后,开始实现History类吧,它的目的是根据用户设定的模式,管理路径,通知 RouterView把路径对应的组件渲染出来。

    在项目根目录新建history/base.js:

    import createRoute from '../util/route'
    export default class History {
      constructor (router) {
        this.router = router
        // 记录当前路径对应的 route 对象 { matched, path }
        this.current = createRoute(null, '/')
      }
    
      transitionTo (path, onComplete) {
        this.current = this.router.matcher.match(path)
        onComplete && onComplete()
      }
    }
    

    创建时当时路径先默认为根路径,current是路由对象,属性有路径名和相关资料,transitionTo是路径跳转时调用的方法,它更改current和调用回调函数。 之后不同模式(如hash或history)的类都是继承History。这里只实现HashHistory:

    import History from './base'
    export default class HashHistory extends History {
      constructor (router) {
        super(router)
        // 保证首次访问的时候 #/
        ensureSlash()
      }
    
      getCurrentLocation () {
        return window.location.hash.slice(1)
      }
    
      setUpListener () {
        window.addEventListener('hashchange', () => {
          this.transitionTo(this.getCurrentLocation())
        })
      }
    }
    
    function ensureSlash () {
      if (window.location.hash) {
        return
      }
      window.location.hash = '/'
    }
    

    HashHistory基本是围绕window.location.hash,所以先讲一下它。简单来说,它会返回#后面的路径名。如果对它赋值,它会在最前面加上#。明白window.location.hash后,其他方法都不难理解。setUpListener注册一个hashchange事件,表示当哈希路径(#后的路径)发生变化,调用注册的函数。

    html5模式不实现了,继承HashHistory算了:

    import History from './base'
    export default class HTML5History extends History {
    }
    

    History的类基本实现了,但是现在还不是响应式的,意味着即使实例发生变化,视图不会变化。这问题后解决。

    回到VueRouter的构造函数:

    constructor(options)
    ...
        const mode = this.mode = options.mode || 'hash'
    
        switch (mode) {
          case 'hash':
            this.history = new HashHistory(this)
            break
          case 'history':
            this.history = new HTML5History(this)
            break
          default:
            throw new Error('mode error')
        }
     }
    

    这里使用了简单工厂模式 (Simple Factory Pattern),就是设计模式中工厂模式的简易版。它存有不同的类,这些类都是继承同一类的,它通过传入的参数进行判断,创建相应的实例返回。简单工厂模式的好处是用户不用考虑创建实例的细节,他要做的是导入工厂,往工厂传入参数,就可获得实例。

    init

    之前的History有一个问题,就是它不是响应式的,也就是说,路径发生变化,浏覧器不会有任何反应,要想为响应式,可以给它一个回调函数:

    import createRoute from '../util/route'
    export default class History {
      constructor (router) {
      ...
        this.cb = null
      }
      ...
      listen (cb) {
        this.cb = cb
      }
      
      transitionTo (path, onComplete) {
        this.current = this.router.matcher.match(path)
    
        this.cb && this.cb(this.current)
        onComplete && onComplete()
      }
    }
    

    加上listen方法,为History添加回调函数,当路径发生转变时调用。

    把之前的初始化方法init补上:

    init (app) {
      // app 是 Vue 的实例
      const history = this.history
    
      history.listen(current => {
        app._route = current
      })
    
      history.transitionTo(
        history.getCurrentLocation(),
        history.setUpListener
      )
    }
    

    给history的回调函数是路径发生变化,把路由传给vue实例,然后是转换至当前路径,完成时调用history.setUpListener。不过直接把history.setUpListener放进去有一个问题,因为这等于是仅仅把setUpListener放进去,里面的this指向window,所以要用箭头函数封装,这样的话,就会调用history.setUpListener,this指向history。

      init (app) {
        // app 是 Vue 的实例
        const history = this.history
    
        const setUpListener = () => {
          history.setUpListener()
        }
    
        history.listen(current => {
          app._route = current
        })
    
        history.transitionTo(
          history.getCurrentLocation(),
          setUpListener
        )
      }
    

    用箭头函数把history.setUpListener封装一下,this就指向history。

    install补完

    init完成实现,回来把install的剩馀地方实现了。当初始化完成后,把vue实例的路由(不是路由表)变成响应式,可以使用 Vue.util.defineReactive(this, '_route', this._router.history.current),就是为vue实例添加一个属性_route,它的值是this._router.history.current,最后添加routerrouter和router和route。 完整代码如下:

    import Link from './components/link'
    import View from './components/view'
    
    export let _Vue = null
    export default function install (Vue) {
      // 判断该插件是否注册略过,可以参考源码
      _Vue = Vue
      // Vue.prototype.xx
      _Vue.mixin({
        beforeCreate () {
          // 给所有 Vue 实例,增加 router 的属性
          // 根实例
          // 以及所有的组件增加 router 属性
          if (this.$options.router) {
            this._router = this.$options.router
            this._routerRoot = this
            // 初始化 router 对象
            this._router.init(this)
            Vue.util.defineReactive(this, '_route', this._router.history.current)
    
            // this.$parent
            // this.$children
          } else {
            this._routerRoot = this.$parent && this.$parent._routerRoot
          }
        }
      })
    
      _Vue.component(Link.name, Link)
      _Vue.component(View.name, View)
    
      Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
      })
    
      Object.defineProperty(Vue.prototype, '$route', {
        get () { return this._routerRoot._route }
      })
    }
    

    现在就可以如平时开发一样,使用routerrouter和router和route。

    RouterView

    最后把RouterView实现。其实它也没什么,就是获取当取路径,从路径中得到组件,然后渲染出来。问题是要考虑父子组件的问题。把思想整理一下,当有父组件时,肯定是父组件已经渲染出来,子组件是从父组件的RouterView组件渲染,还有是$route有的是当前路径和匹配的资料的数组,即包括父组件的数组,所以可遍历获得要渲染的组件:

    export default {
      name: 'RouterView',
      render (h) {
    
        const route = this.$route
        let depth = 0
        //routerView表示已经完成渲染了
        this.routerView = true
        let parent = this.$parent
        while (parent) {
          if (parent.routerView) {
            depth++
          }
          parent = parent.$parent
        }
    
        const record = route.matched[depth]
        if (record) {
          return h(record.component)
        }
        return h()
      }
    }
    

    if (parent.routerView) 是因为是确认父组件是否已经渲染,如果渲染,它的routerView为true,用depth来记录有多少父路由,然后通过它获取matched的资料,有的话则渲染获取的组件。

    总结

    Vue Router的代码量不多,但实在是绕,简单总结一下比较好。先看一下项目结构:

    Vue-Router实现

    用一张表把所有的文件作用简述一遍:

    文件作用
    index.js存放VueRouter类install.js插件类必须要有的函数,用来给Vue.use调用create-route-map.js生成路由表,它输出一个对象,有pathList和pathMap属性,前者是存有所有路径的数组,后者是字典,把路径和它的资料对应util/route.js一个函数接收路径为参数,返回路由对象,存有matched和path属性,matched是匹配到的路径的资料和父路径资料,它是一个数组,path是路径本身create-matcher.js利用create-route-map创建路由表,且返回两个函数,一个是用util/route匹配路由,另一个是手动把路由规则转变成路由history/base.jsHistory类文件,用来作历史管理,存有当前路径的路由,以及转换路径的方法history/hash.jsHashHistory类文件,继承至History,用作hash模式下的历史管理components/link.jsRouter-Link的组件components/view.jsRouter-View的组件

    起源地下载网 » Vue-Router实现

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元