After reading the implementation of vue-Router in the pull-up boot camp, I was full of dried goods, but the implementation of vue-Router is rather convoluted, so take notes, confirm and deepen your understanding. I entered the front end training camp of pull hook for more than two months, and I harvested a lot. There were many great talents in the group, as well as the director of beauty class. The tutor answered students’ questions in time, with humor and wit.

Implemented functions

Before implementing it, take a look at what it does:

  1. Basic Routing function
  2. Child routing function
  3. History and Hash function

Create a project. Create the Vue Router class in the root directory of the index.js file:

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

    init () {}
}
Copy the code

When we create a routing instance, we pass in an object like this:

const router = new VueRouter({
  routes
})
Copy the code

So the constructor should have an object, if there is a route in it, assign it to this._routes, otherwise give it an empty array. Of course there are other properties in options, but forget about them and implement them later. There is also an init method that initializes Settings.

install

Since the Vue Router is a plug-in, you must use it through the vue.use method. This method determines whether the argument passed is an object or a function. If it is an object, the install method is called, or if it is a function, it is called directly. The Vue Router is an object, so you need the install method. Before implementing Install, take a look at the vue. use source code to better understand how to implement 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}}Copy the code

Vue. Use determines whether Vue has an attribute called _installedPlugins and references it if it does. If Vue does not have an attribute called _installedPlugins, it is an empty array and then references it. _installedPlugins is a record of installed plug-ins. Then determine if _installedPlugins have any plugins passed in, and if they do, don’t install them. Start with the second parameter passed in and make it an array, putting Vue at the top of the array. If the plug-in is an object, its install method is called, and the context of the plug-in method is still itself, passing in the parameters that were just changed to an array. Function, regardless of context, directly called. Finally, record that the plug-in is installed.

Now simply implement the install method and create install.js in the root directory:

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
        // Initialize the Router object
        this._router.init(this)}else {
        this._routerRoot = this.$parent && this.$parent._routerRoot
      }
    }
  })
Copy the code

The global variable _Vue is used to facilitate the reference of other Vue Router modules, otherwise other modes need to introduce Vue, which is quite troublesome. Mixins are used to extract some functionality from Vue and reuse it in different places. In this case, the global hook function is used.

$options.router = $options.router = $options.router = $options. For the root instance, add two private attributes, _routerRoot for reference by components below the root instance, and then initialize the Router. If it is a root component, check to see if it has a parent component. If it does, refer to its _routerRoot, so that routes can be referenced via _RouterRoot. router.

The mount function is almost complete. When we use the Vue Router, we also mount two components: the Router Link and the Router View. Create the components folder under the root directory and create the link.js and view.js files. Router Link Router Link

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

RouterLink receives a parameter, to, of type string. We don’t use template here because we don’t have a compiler running this version of VUE, so we convert templates to renderers and use renderers directly. The first parameter is the tag type, the second parameter is the tag attribute, and the third parameter is the content. See the VUE documentation for details. We want to achieve is < a: href = “{{‘ # ‘+ enclosing the to}}” > < slot name = “default” > < / slot > < / a >. So the first argument is a, the second one is its connection, and the third one is an array because the tag is a slot tag node, and the children are wrapped in an array. RouterView RouterView RouterView RouterView

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

Register two components in 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) }Copy the code

createMatcher

Next we create create-matcher, which is used to generate matchers and returns two methods: match and addRoutes. The former method matches the input path to obtain routing table information. The latter method manually adds routing rules to the routing table. Both methods rely on routing tables, so we need to implement a routing table generator: Create-router-map, which receives routing rules, returns a routing table, which is an object with two properties in it. One is a pathList, which is an array containing the paths of all the routing tables, and the other is a pathMap, which is a dictionary, where the keys are paths and the values of the paths correspond to the data. Create create-router-map.js in the project root directory:

export default function createRouteMap (routes) {

  // Store all routing addresses
  const pathList = []
  // Routing table, path, and component information
  const pathMap = {}

  return {
    pathList,
    pathMap
  }
}
Copy the code

We need to traverse the routing rules and do two things in the process:

  1. Put all paths into the pathList
  2. Put the route and data mapping into the pathMap

The difficulty here is that there are subpaths, so recursion is needed, but don’t worry about that for now, simply implement the function:

function addRouteRecord (route, pathList, pathMap, parentRecord) {
  const path = route.path
  const record = {
    path: path,
    component: route.component,
    parentRecord: parentRecord
    // ...
  }

  // Check whether the current path is already stored in the routing table
  if(! pathMap[path]) { pathList.push(path) pathMap[path] = record } }Copy the code

Now consider routing all at once. First of all, we need to determine whether the route has child routes, if so, we need to traverse the child routes, recursive processing, and also consider the path name problem, if it is a child path, path should be a parent path combination, so we need to determine whether there is a parent route.

function addRouteRecord (route, pathList, pathMap, parentRecord) {
  const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path
  const record = {
    path: path,
    component: route.component,
    parentRecord: parentRecord
    // ...
  }

  // Check whether the current path is already stored in the routing table
  if(! pathMap[path]) { pathList.push(path) pathMap[path] = record }// Check whether the current route has children
  if (route.children) {
    route.children.forEach(childRoute= > {
      addRouteRecord(childRoute, pathList, pathMap, route)
    })
  }
}
Copy the code

If the parent route information is passed in, path is merged with the parent path.

Add addRouteRecord to createRouteMap:

export default function createRouteMap (routes) {
  // Store all routing addresses
  const pathList = []
  // Routing table, path, and component information
  const pathMap = {}

  // Run the routes command
  routes.forEach(route= > {
    addRouteRecord(route, pathList, pathMap)
  })

  return {
    pathList,
    pathMap
  }
}
Copy the code

CreateRouteMap creates a routing table by creating a routing table by 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
  }
}
Copy the code

Finally, it implements match, which takes a path and returns information about the path, not only about itself, but about its parent path. Here we implement a utility class function that creates a route, which returns the path and its associated data. Create util/route. Js:

export default function createRoute (record, path) {
  // Create a routing data object
  // route ==> { matched, path } matched ==> [record1, record2]
  const matched = []

  while (record) {
    matched.unshift(record)

    record = record.parentRecord
  }

  return {
    matched,
    path
  }
Copy the code

In fact, the function is very simple, is to constantly get the data at the top of the array. With createRoute, match is basically implemented:

import createRoute from './util/route'

  function match (path) {
    const record = pathMap[path]
    if (record) {
      // Create a routing data object
      // route ==> { matched, path } matched ==> [record1, record2]
      return createRoute(record, path)
    }
    return createRoute(null, path)
  }
Copy the code

Add matcher to VueRouter’s constructor:

import createMatcher from './create-matcher'

export default class VueRouter {
  constructor (options) {
    this._routes = options.routes || []
    this.matcher = createMatcher(this._routes)
...
Copy the code

History History Management

Once the matcher is ready, implement the History class, which manages paths based on user-specified patterns and tells The RouterView to render the components corresponding to the paths.

Create history/base.js in the project root directory:

import createRoute from '.. /util/route'
export default class History {
  constructor (router) {
    this.router = router
    // The route object corresponding to the current path {matched, path}
    this.current = createRoute(null.'/')
  }

  transitionTo (path, onComplete) {
    this.current = this.router.matcher.match(path)
    onComplete && onComplete()
  }
}
Copy the code

Current is the route object, and properties include the path name and related information. TransitionTo is the method called when a path is transitioned. It changes current and calls the callback function. Subsequent classes with different schemas, such as hash or history, inherit history. Only HashHistory is implemented here:

import History from './base'
export default class HashHistory extends History {
  constructor (router) {
    super(router)
    // Ensure that the first access is #/
    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 = '/'
}
Copy the code

HashHistory is basically around window.location.hash, so let’s talk about it first. Simply put, it returns the pathname after #. If you assign a value to it, it will prefix it with #. Once you understand window.location.hash, the other methods are easy to understand. SetUpListener registers a hashchange event that calls the registered function when the hash path (the path after #) changes.

Html5 mode is no longer implemented, instead inherit HashHistory:

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

The History class is basically implemented, but it’s not reactive yet, meaning that the view doesn’t change even if the instance changes. The problem will be solved later.

Back to VueRouter’s constructor:

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')}}Copy the code

The Simple Factory Pattern is used here, which is a simplified version of the Factory Pattern in the design Pattern. It has different classes, which all inherit from the same class. It determines by passing in parameters and creates corresponding instances to return. The advantage of the simple factory pattern is that the user doesn’t have to worry about the details of creating an instance. All he has to do is import the factory, pass in parameters to the factory, and get the instance.

init

The problem with History is that it is not responsive, that is, the path changes and the browser does not respond. To be responsive, you can give it a callback function:

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()
  }
}
Copy the code

Add the listen method to the History callback function, which is called when the path changes.

Init ();

init (app) {
  // app is an instance of Vue
  const history = this.history

  history.listen(current= > {
    app._route = current
  })

  history.transitionTo(
    history.getCurrentLocation(),
    history.setUpListener
  )
}
Copy the code

The callback to history isto change the path, pass the route to the vue instance, then convert to the current path, and call history.setUpListener when done. But there’s a problem with putting history.setUpListener in there, because that’s just putting a setUpListener in there, and the this in there points to the window, so it needs to be wrapped in an arrow function, and in that case, history.setUpListener, This refers to history.

  init (app) {
    // app is an instance of Vue
    const history = this.history

    const setUpListener = () = > {
      history.setUpListener()
    }

    history.listen(current= > {
      app._route = current
    })

    history.transitionTo(
      history.getCurrentLocation(),
      setUpListener
    )
  }
Copy the code

Enclose history.setUpListener with the arrow function. This points to history.

Install to fill out

Init completes the implementation and comes back to implement the rest of install. DefineReactive (this, ‘_route’, this._router.history.current), Add a _route attribute to the vue instance, which has the value this._router.history.current, and finally add router and Router and route. The complete code is as follows:

import Link from './components/link'
import View from './components/view'

export let _Vue = null
export default function install (Vue) {
  // Check whether the plug-in registered skipped, can refer to the source code
  _Vue = Vue
  // Vue.prototype.xx
  _Vue.mixin({
    beforeCreate () {
      // Add router attributes to all Vue instances
      / / root instance
      // Add router attributes to all components
      if (this.$options.router) {
        this._router = this.$options.router
        this._routerRoot = this
        // Initialize the Router object
        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 }
  })
}
Copy the code

You can now use the Router and router and Route as you would normally develop.

RouterView

Finally, implement the RouterView. It’s nothing, it’s just get when you take the path, get the component from the path, and render it. The problem is to consider parent-child components. $route is an array of the current path and matching data, so it is possible to iterate over the component to render: $route is an array of the current path and matching data.

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

    const route = this.$route
    let depth = 0
    //routerView indicates that the rendering is complete
    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()
  }
}
Copy the code

If (parent. RouterView) is used to check whether the parent component is already rendered. If it is rendered, its routerView is true, and how many parent routes are recorded by depth.

conclusion

The Vue Router code is not much, but it is rather convoluted, so it is a good summary. Take a look at the project structure:

Use a list to briefly describe the functions of all the documents:

file role
index.js Store VueRouter class
install.js The plugin class must have a function to call vue.use
create-route-map.js Generates a routing table that outputs an object with pathList, an array of all paths, and pathMap, a dictionary that maps a path to its data
util/route.js A function receives path as an argument and returns a route object with matched and path attributes. Matched is the matched path information and parent path information. It is an array and path is the path itself
create-matcher.js Use create-route-map to create a routing table and return two functions, one is util/route matching and the other is manual routing rules into routes
history/base.js The History class file, used for History management, holds the route to the current path, and the method to convert the path
history/hash.js HashHistory class file, inherited to History for History management in hash mode
components/link.js The Router – the Link component
components/view.js The Router – the View components