The reason was that our team encountered a fundamental routing problem while migrating the distribution system internally. I vaguely knew why at that time, but we were so familiar with routing that we forgot many of its basic features and did not quickly troubleshoot the problems in the first time. I feel ashamed of it.

So I find a time to fill up the basic knowledge of the relevant route, and look at the vue-Router source code, in general, first talk about vue-Router is how to combine with VUE to do routing management. More detailed source code analysis will follow

The main content of this article is as follows

  • What is routing
  • The back-end routing
  • The front-end routing
  • vue-router
    • How to inject vueRouter into vUE (plug-in installation)
    • init VueRouter
    • The constructor VueRouter
    • How to change a URL
    • How to render after URL change
    • How to deal with go, forward and back
    • setupListeners
    • Hash routing
    • The history of routing

routing

Since we’re going to talk about front-end routing, we should know what routing is first.

The routing concept was originally brought up by the back end. In the early days, it was all server-side rendering, before the front and back ends were separated, and the server would return the entire page, and the response would look something like this:

  1. The browser makes a request
  2. The server’s interfaces 80 or 443 listen for requests from the browser and parse the URL path
  3. According to the URL content, the server queries the corresponding resources, which may be HTML resources or image resources…. The corresponding resource is then processed and returned to the browser
  4. The browser receives the data and then decides based on the Content-Type how to parse the resource

So what is routing? We can simply understand as a way to interact with the server, through different routes we request different resources (HTML resources is just one of the ways).

The back-end routing

What we described above is actually back-end routing.

Back-end routing is also called server-side routing, because when a server receives an HTTP request from a client, it finds the corresponding mapping function based on the REQUESTED URL, executes the function, and sends the return value of the function to the client.

For the simplest static resource server, you can think of the mapping function of all urls as a file read operation. For dynamic resources, the mapping function may be a database read operation, some data processing, and so on.

Then according to these read data, the server side will use the corresponding template to render the page, and then return the rendered page.

The front-end routing

Front-end routing was born out of the rise of Ajax, which we all know is a browser for asynchronous loading technology, as I just explained, in the case of the front and back end is not separated, the server will directly return the entire HTML, Every small operation of the user will cause the entire page refresh (plus the previous Internet speed is still slow, so the user experience can be imagined).

In the late 90’s, Microsoft first implemented Ajax (Asynchronous JavaScript And XML) technology, which greatly improves the user experience without having to refresh the entire page for each operation.

With the development of technology, the three frames dominate the front end circle and become the main force of the front end development. The front end can also do more things, and gradually there are modular and componentized concepts.

Of course, there are single-page applications, MVVM has also appeared in front of the VISION of ER.

At this point, front-end developers are able to develop larger applications and become more powerful, so what does this have to do with front-end routing?

A more advanced version of the asynchronous interaction experience is SPA, a single page application. Single-page applications are not only refresh-free for page interactions, but also for page jumps. Since the jump to the page is refresh-free, that is, the backend request is no longer returned to THE HTML.

So, a large application will usually have dozens of pages (URL address) jump to each other, how does the front end know what url corresponding display content?

The answer is front-end routing

It can be understood that front-end routing is the front-end to do the task of the server to return different pages according to the different URL.

Advantages: good user experience, do not need to capture all from the server, and every time a quick show users faults: use the browser’s forward and back button will send the request again, there is no reasonable use of caching, single page not remember before the position of the scroll, not in advance, to remember the location of the rolling back.

What problem does front-end routing solve

  • Front-end routing allows the front-end to maintain the routing and display logic of the page itself. Every page change does not need to notify the server.
  • Better interactive experience: No need to pull resources from the server every time and quickly show them to the user

What are the disadvantages of front-end routing?

  • The most notorious thing is that it’s bad for SEO
  • Using the browser’s forward and back keys will re-send requests to retrieve data without making proper use of caching.

What is the implementation principle of front-end routing

After understanding what front-end routing is and what problems front-end routing solves, let’s take a closer look at how front-end routing works

The implementation principle of front-end routing is actually very simple. In essence, it detects URL changes, intercepts URLS and then resolves matching routing rules.

Hash routing

In the old days, people used hash to implement routing, and hash routing is just like that<a>The anchor point of the link is the same, added after the address#For example, my personal blog https://cherryblog.site/#/  #And what follows, we call the hash of location

Then we click on the other TAB pages and notice that although the URL in the browser’s address bar has changed, the page has not been refreshed. When we open the console, we can see that switching the TAB simply sends the server the interface that requested the interface data, and does not re-request the HTML resource.This is because a hash change does not cause the browser to send a request to the server, so the page is not refreshed. But every time the hash changes, it is triggeredhaschangeEvents. So we can use a wiretaphaschangeIn response to changes.

In our current (2021) front-end development, there is usually a root node<div id="root"></div>, and then insert what you want to show into the root node. The inserted content component is then replaced according to the route.

The history of routing

One problem with hash routing is that it’s not “pretty” because it has #

Fourteen years later, the HTML5 standard was released. Two new apis, pushState and replaceState, allow you to change urls without sending requests. There’s also an onPopState event. This allows you to implement front-end routing in a different way, but in the same way that you implement hash.

With HTML5 implementations, single-page routing urls don’t have an extra #, making them more aesthetically pleasing. But because there are no # numbers, the browser still sends a request to the server when the user does something like refresh the page. To avoid this, the implementation requires server support to redirect all routes to the root page. [HTML5 Histroy mode](HTML5 History mode)

Notice the direct callhistory.popState() 和 history.poshState()It doesn’t triggerpopState. Called only if browser behavior has been performedpopStateSuch as clicking the browser’s forward and back buttons or making JS callshistory.back()orhistory.forward() 

vue-router

Let’s take a look at how vue-Router implements front-end routing with VUE.

The general idea is to use vue.util.definereActive to set the _route of the instance to a reactive object. The push, replace methods actively update the _route attribute. Go, back, or clicking the forward and back buttons will update _route in the onHashchange or onPopState callback. The _route update triggers a rerendering of the RoterView.

And then we’re going to look at how does that work

How to inject vueRouter into vUE (plug-in installation)

Vue provides a plug-in registration mechanism. Each plug-in needs to implement a static install method. When a plug-in is registered with vue. use, install is executed, and the first argument to this method is a Vue object.

In vue-Router, install is as follows.

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

// Export the vue instance
export let _Vue

Use (vueRouter) is equivalent to vue.use (vueRouter. Install ()).
export function install (Vue) {
  // If already registered and already has vUE instance, then return directly
  if (install.installed && _Vue === Vue) return
  install.installed = true

  // Save the Vue instance for other plug-in files
  _Vue = Vue

  const isDef = v= >v ! = =undefined

  // A recursive method to register instances
  const registerInstance = (vm, callVal) = > {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  /** * Recursively mix all vUE components with two life cycles beforeCreate and destroyed * to initialize vuE-router in beforeCreated and _route response */
  Vue.mixin({
    beforeCreate () {
      // Initialize vue-router
      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 {
        // make _route a responsive object
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this.this)
    },
    destroyed () {
      registerInstance(this)}})$router = router instance * $route = current route */
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

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

  /** * Inject two global components * 
      
        * 
       
         */
       
      
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  /** * Vue. Config is an object that contains the global configuration of Vue
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
Copy the code

To ensure that the VueRouter is executed only once, an identity installed is added when the install logic is executed. Use a global variable to save the Vue to facilitate plug-in use of the Vue.

The core of the VueRouter installation is to mix beforeCreate and destroyed hook functions into all components of the Vue app via mixin.

It also adds instance objects to Vue

  • _routerRoot: Points to the Vue instance
  • _router: points to instance vueRouter

Initialize some getters on Vue’s Prototype

  • $router, the instance of the current router
  • $route, information about the current Router

Vue.util. DefineReactive, which is the way in Vue that the observer hijacks data, hijacking _route, notifying dependent components when _route fires setter methods.

The Vue.component method defines two global

and

components.

is similar to the A tag.

is the route exit. In

, the route is switched to render different Vue components. Finally, the merging strategy of route guard is defined and the merging strategy of Vue is adopted.




init VueRouter

We just mentioned that VueRouter’s init method (this._router.init(this)) is executed during install, so let’s look at what the init method does. Simply mount the Vue instance to the current router instance.

VueRouter’s init method (this._router.init(this)) is then executed during install. Init uses history.transitionto to transition routes. Matcher route matcher is the core function of route switching, route and component matching.


  init (app: any /* Vue component instance */) {
    this.apps.push(app)

    // main app previously initialized
    // return as we don't need to set up new history listener 
    if (this.app) {
      return
    }

    // Mount the Vue instance on VueRouter
    this.app = app

    const history = this.history

    // Hashchange events are monitored in setupListeners
    // transitionTo is the function used to perform route navigation
    if (history instanceof HTML5History || history instanceof HashHistory) {
      const setupListeners = routeOrError= > {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }

    // Route global listener to maintain the current route
    // Since _route is defined as a responsive attribute when install executes,
    // When route changes _route updates, subsequent view updates render depending on _route
    history.listen(route= > {
      this.apps.forEach(app= > {
        app._route = route
      })
    })
  }
Copy the code

The constructor VueRouter

VueRouter’s constructor is relatively simple

  • Attributes and methods are defined.
  • Create a matcher matching function that looks for a route
  • Set defaults and do a downgrade that does not support H5 history
  • Instantiate different History objects based on different modes
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // Create a matcher matching function
    this.matcher = createMatcher(options.routes || [], this)

    // Hash routes are used by default
    let mode = options.mode || 'hash'
    
    // H5 history is compatible
    this.fallback =
      mode === 'history'&&! supportsPushState && options.fallback ! = =false
    if (this.fallback) {
      mode = 'hash'
    }
    
    if(! inBrowser) { mode ='abstract'
    }
   
    this.mode = mode

    // Distribution processing
    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}`)}}}Copy the code

When instantiating the vueRouter, it defines apis like History: Push, replace, back, Go, forward, routing matchers, adding router dynamic update methods, and so on.

How to change a URL

So how does the VueRouter do this? $router. Push (‘/foo’, increment) how do we render the view to display the foo component when using _this_.$router.

const router = new VueRouter({
  mode: 'history'.base: __dirname,
  routes: [{path: '/'.component: Home },
    { path: '/foo'.component: Foo },
    { path: '/bar'.component: Bar },
    { path: encodeURI('/ e'), component: Unicode },
    { path: '/query/:q'.component: Query }
  ]
})
Copy the code

Remember what we just did in vue-Router constructor? Let me refresh your memory. From constructor we select different types of history to instantiate according to different modes (h5 history or hash history or abstract). Then call history.transitionto during init to initialize the route and complete the first route navigation.

The transitionTo method can be found in the history/base.js file. TransitionTo can accept location, onComplete, and onAbort, which are callbacks to the target path, successful path switchover, and failed path switchover.

The incoming location is found in the Router, the current route is updated, and the path switch succeeds callback is executed (in which the history is implemented differently for different modes).

Either the replaceHash or pushHash methods are called in the callback. They update the hash value of the location. If compatible with historyAPI, history.replaceState or history.pushState will be used. If not, the historyAPI will use window.location.replace or window.Location. hash.

The handleScroll method updates the position of our scrollbar.

transitionTo ( location: RawLocation, onComplete? :Function, onAbort? :Function
) {
    // Call match to get a matching route object
    const route = this.router.match(location, this.current)
    
    // Transition processing
    this.confirmTransition(
        route,
        () = > {
            // Update the current route object
            this.updateRoute(route)
          
            // Update hash mode of URL address Update hash value History mode is updated using pushState/replaceState
            onComplete && onComplete(route)
           
            this.ensureURL()
    
            // fire ready cbs once
            if (!this.ready) {
                this.ready = true
                this.readyCbs.forEach(cb= > {
                  cb(route)
                })
            }
        },
        err= > {
            if (onAbort) {
                onAbort(err)
            }
            if (err && !this.ready) {
                this.ready = true
                this.readyErrorCbs.forEach(cb= > {
                cb(err)
                })
            }
        }
    )
}
Copy the code

How to render a component after a URL change

At this point, you can make history objects in different modes have the same push replace functionality (see the implementation below for details)

So how to render correctly after the route change.

Remember the reactive principle of VUE we talked about earlier? We already set _router to be responsive during install. Any change to the _router triggers a rendering of the RouterView. (We updated _route in the transitionTo callback)

go, forward, back

The go, forward, and back methods defined on VueRouter are all go methods that call the history property.

How does the go method on the hash call history.go update the RouteView? The answer is that the Hash object has a popState or Hashchange listener added to the setupListeners method. An update to the RoterView is triggered in the event’s callback

setupListeners

When we click the back and forward buttons or call the back, forward and go methods. We did not actively update _app.route and current. How do we trigger updates to the RouterView? By listening for popState or hashchange events on the window. In the event callback, the _route and current updates are completed by calling the transitionTo method.

Alternatively, when using the push, replace method, the hash update comes after the _route update. With go, back, the hash update precedes the _route update.

setupListeners () { const router = this.router const expectScroll = router.options.scrollBehavior const supportsScroll =  supportsPushState && expectScroll if (supportsScroll) { setupScroll() } window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => { const current = this.current if (! ensureSlash()) { return } this.transitionTo(getHash(), route => { if (supportsScroll) { handleScroll(this.router, route, current, true) } if (! supportsPushState) { replaceHash(route.fullPath) } }) }) }Copy the code

Hash routing

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())
    }

    // Add a hashchange event listener
    window.addEventListener(
      hashchange,
      () = > {
        const current = this.current
        // Get the hash content and render the new page to the node of the UI-View via route configuration
        this.transitionTo(getHash(), route= > {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)}if(! supportsPushState) { replaceHash(route.fullPath) } }) } )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)
  }
}

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

H5 history routing

In fact, the implementation of hash is basically similar, the main difference is that

  • Listen for different events
  • The push and replace methods are implemented differently
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())
    }

    // By listening for popState events
    window.addEventListener('popstate'.() = > {
      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)}})})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= > {
      // Using pushState to update the URL does not cause the browser to send a request and thus not refresh the page
      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 differs from pushState in that it is not recorded in the history stack
      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)
  }
}

Copy the code

Can read here students really thank you ~~ this is my first time to write source code related content, has not studied very thoroughly, which will inevitably have some mistakes, I hope you correct ~~~