The front-end routing

routing

Routing is the distribution of requests and the transmission of information from the original address to the destination address over the network

The front-end routing

define

In an HTML page to achieve interaction with the user without refreshing and skipping pages, at the same time, match a special URL for each view display in SPA, change this URL and do not let the browser like the server to send requests, and can listen to the CHANGE of URL.

SPA

Single Page Web application

Simply speaking, SPA is a WEB project with only one HTML page. Once the page is loaded, SPA will not reload or jump the page because of the user’s operation. Instead, JS is used to dynamically change the content of HTML to simulate jumping between views.

Traditional page —

Each HTML page is completed by an HTML file that contains the complete HTML structure

SPA page –

An application has only one HTML file, but the HTML file contains a placeholder, and the content of the placeholder is determined by the view. The page switch of SPA is the view switch

Hash mode, History mode, memory mode

Hash pattern

The sample

Usage scenarios

You can use front-end routing in any situation

disadvantages

SEO is not friendly and the server cannot receive hash.

For example, if you visit http://www.baidu.com/#river, the Request URL is http://www.baidu.com/

The history mode

The sample

Usage scenarios

When the back end renders all front-end routes to the same page (not the 404 page), you can use history mode

disadvantages

Internet Explorer 8 and below are not supported

The memory model

The sample

different

Unlike hash mode and History mode, which store the path by URL, memory mode stores the path in local storage, and mobile mode stores the path in mobile database.

disadvantages

Valid for single machine only

VueRouter source

The official documentation

To realize the Vue Router

Using Vue Router(Hash mode)

To use history mode, simply add

const router = new VueRouter({
  mode: "history",
  routes
})
Copy the code

Vue-router source code analysis

The source code

Let’s first look at the implementation path of VUE.

An instance object of VueRouter needs to be instantiated in the entry file and passed into the Options of the Vue instance.

export default class VueRouter {
  static install: () = > void;
  static version: string;

  app: any;
  apps: Array<any>;
  ready: boolean;
  readyCbs: Array<Function>;
  options: RouterOptions;
  mode: string;
  history: HashHistory | HTML5History | AbstractHistory;
  matcher: Matcher;
  fallback: boolean;
  beforeHooks: Array<? NavigationGuard>; resolveHooks:Array<? NavigationGuard>; afterHooks:Array<? AfterNavigationHook>;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)
    // Instantiate the specific History according to mode, default to 'hash' mode
    let mode = options.mode || 'hash'
    SupportsPushState check whether the browser supports the 'history' mode
    // If 'history' is set but the browser does not support it, 'history' mode will revert to 'hash' mode
    PushState controls whether a route should revert to hash mode when the browser does not support history.pushState. The default value is true.
    this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false
    if (this.fallback) {
      mode = 'hash'
    }
    // If it is not inside the browser, it becomes 'abstract' mode
    if(! inBrowser) { mode ='abstract'
    }
    this.mode = mode
     // Select the corresponding History class to instantiate according to the different schema
    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}`) } } } match ( raw: RawLocation, current? : Route, redirectedFrom? : Location ): Route {return this.matcher.match(raw, current, redirectedFrom) } get currentRoute (): ? Route {return this.history && this.history.current
  }

  init (app: any /* Vue component instance */) { process.env.NODE_ENV ! = ='production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )

    this.apps.push(app)

    // main app already initialized.
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history
    // Perform initialization and listening operations according to the category of history
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () = > {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    history.listen(route= > {
      this.apps.forEach((app) = > {
        app._route = route
      })
    })
  }
  // Route before jump
  beforeEach (fn: Function) :Function {
    return registerHook(this.beforeHooks, fn)
  }
  // Route navigation is confirmed between
  beforeResolve (fn: Function) :Function {
    return registerHook(this.resolveHooks, fn)
  }
  // After a route jump
  afterEach (fn: Function) :Function {
    return registerHook(this.afterHooks, fn)
  }
  // The callback function called when the first hop is complete
  onReady (cb: Function, errorCb? :Function) {
    this.history.onReady(cb, errorCb)
  }
  // Routing error reported
  onError (errorCb: Function) {
    this.history.onError(errorCb)
  }
  // Route add, this method adds a record to the history stack, click back to return to the previous page.push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    this.history.push(location, onComplete, onAbort)
  }
  // This method does not add a new record to history. Click Back to switch to the previous page. The last record does not exist.replace (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    this.history.replace(location, onComplete, onAbort)
  }
  // How many pages to jump forward or backward from the current page, like window.history.go(n). N can be positive or negative. A positive number returns the previous page
  go (n: number) {
    this.history.go(n)
  }
  // Go back to the previous page
  back () {
    this.go(-1)}// Proceed to the next page
  forward () {
    this.go(1) } getMatchedComponents (to? : RawLocation | Route):Array<any> {
    const route: any = to
      ? to.matched
        ? to
        : this.resolve(to).route
      : this.currentRoute
    if(! route) {return[]}return [].concat.apply([], route.matched.map(m= > {
      return Object.keys(m.components).map(key= > {
        return m.components[key]
      })
    }))
  }

  resolve (
    to: RawLocation,
    current?: Route,
    append?: boolean
  ): {
    location: Location,
    route: Route,
    href: string,
    // for backwards compat
    normalizedTo: Location,
    resolved: Route
  } {
    const location = normalizeLocation(
      to,
      current || this.history.current,
      append,
      this
    )
    const route = this.match(location, current)
    const fullPath = route.redirectedFrom || route.fullPath
    const base = this.history.base
    const href = createHref(base, fullPath, this.mode)
    return {
      location,
      route,
      href,
      // for backwards compat
      normalizedTo: location,
      resolved: route
    }
  }

  addRoutes (routes: Array<RouteConfig>) {
    this.matcher.addRoutes(routes)
    if (this.history.current ! == START) {this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}
Copy the code

HashHistory

• Hash appears in the URL, but is not included in the HTTP request. It is used to direct browser actions and has no impact on the server, so changing the hash does not reload the page.

• You can add listening events for hash changes:

window.addEventListener("hashchange",funcRef,false)
Copy the code

• Each change to hash(window.location.hash) adds a record to the browser’s access history.

export class HashHistory extends History {
  constructor (router: Router, base: ? string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    // If you are demoted from history mode, you need to do a demotion check
    if (fallback && checkFallback(this.base)) {
    // Returns if retracted and retracted
      return
    }
    ensureSlash()
  }
  .......
function checkFallback (base) {
  const location = getLocation(base)
  // Get the real location value excluding base
  if (!/ # ^ / / /.test(location)) {
  // If the address does not start with /#
  // It needs to be degraded to the hash mode /#
    window.location.replace(
      cleanPath(base + '/ #' + location)
    )
    return true}}function ensureSlash () :boolean {
// Get the hash value
  const path = getHash()
  if (path.charAt(0) = = ='/') {
   // If it starts with /, just return
    return true
  }
  // If not, you need to manually replace the hash value once
  replaceHash('/' + path)
  return false
}

export function getHash () :string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  // Because of compatibility issues, window.location.hash is not used directly here
  // Because Firefox decode hash value
  const href = window.location.href
  const index = href.indexOf(The '#')
  return index === -1 ? ' ' : decodeURI(href.slice(index + 1))}// Get the url before the hash
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf(The '#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}
// Add a hash
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}
/ / replace the hash
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
Copy the code

Hash changes are automatically added to the browser’s access history. To see how this is done, look at the transitionTo() method:

transitionTo (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    const route = this.router.match(location, this.current) // Find a matching route
    this.confirmTransition(route, () = > { // Verify whether to convert
      this.updateRoute(route) / / update the route
      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) })
      }
    })
  }
  
// Update the route
updateRoute (route: Route) {
    const prev = this.current // Pre-hop route
    this.current = route // configure a jump route
    this.cb && this.cb(route) This callback is registered in the index file and updates the hijacked _router
    this.router.afterHooks.forEach(hook= > {
      hook && hook(route, prev)
    })
  }
}

Copy the code
pushState
export function pushState (url? : string, replace? : boolean) {
  saveScrollPosition()
  // try... catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  / / added a try... Catch is because Safari has a limit of 100 calls to pushState
  // DOM Exception 18 is thrown when reached
  const history = window.history
  try {
    if (replace) {
    // replace the key is the current key and no need to generate a new one
      history.replaceState({ key: _key }, ' ', url)
    } else {
    // Regenerate key
      _key = genKey()
       // enter the new key value
      history.pushState({ key: _key }, ' ', url)
    }
  } catch (e) {
  // specify a new address when the limit is reached
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

Copy the code
replaceState
// Call pushState directly and pass replace to true
export function replaceState (url? : string) {
  pushState(url, true)}Copy the code

The common feature of pushState and replaceState is that when they are called to modify the browser history stack, the browser does not immediately send a request for the current URL even though it has changed. This provides a basis for single-page front-end routing, updating the view without rerequesting the page.

supportsPushState
export const supportsPushState = inBrowser && (function () {
  const ua = window.navigator.userAgent

  if (
    (ua.indexOf('Android 2.')! = = -1 || ua.indexOf('the Android 4.0')! = = -1) &&
    ua.indexOf('Mobile Safari')! = = -1 &&
    ua.indexOf('Chrome') = = = -1 &&
    ua.indexOf('Windows Phone') = = = -1
  ) {
    return false
  }

  return window.history && 'pushState' in window.history
})()

Copy the code

When the _route value changes, the render() method of the Vue instance is automatically called to update the view. $router.push()–>HashHistory.push()–>History.transitionTo()–>History.updateRoute()–>{app._route=route}–>vm.render()

Listening address bar

In the browser, the user can type the change route directly into the browser address bar, so you also need to listen for the change of route in the browser address bar and have the same response behavior as calling it through code. In HashHistory this is done with setupListeners listening for Hashchange:

setupListeners () {
    window.addEventListener('hashchange'.() = > {
        if(! ensureSlash()) {return
        }
        this.transitionTo(getHash(), route= > {
            replaceHash(route.fullPath)
        })
    })
}

Copy the code

HTML5History

History Interface is the interface provided by the browser History stack. By using methods such as back(),forward(), and Go (), we can read the browser History stack information and perform various jump operations.

export class HTML5History extends History {
  constructor (router: Router, base: ? string) {
    super(router, base)

    const expectScroll = router.options.scrollBehavior // Indicates the rollback mode
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      setupScroll()
    }

    const initLocation = getLocation(this.base)
    // Monitor popState events
    window.addEventListener('popstate'.e= > {
      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.
      // Avoid issuing "popState" events for the first time in some browsers
      The history route was not updated at the same time due to asynchronous listening at the same time.
      const location = getLocation(this.base)
      if (this.current === START && location === initLocation) {
        return
      }

      this.transitionTo(location, route= > {
        if (supportsScroll) {
          handleScroll(router, route, current, true)}})})}Copy the code

Hash mode only changes the contents of the hash part, which is not included in the HTTP request (hash with #) :

oursite.com/#/user/id // If requested, only oursite.com/ will be sent

Therefore, page requests based on urls are not a problem in hash mode

The history mode changes the URL to the same as the normal request back end (history without #).

oursite.com/user/id

If the request is sent to the back end and the back end is not configured to handle the /user/ ID GET route, a 404 error will be returned.

The official recommended solution is to add a candidate resource on the server that covers all cases: if the URL doesn’t match any static resource, it should return the same index.html page that your app relies on. At the same time, the server no longer returns the 404 error page because the index.html file is returned for all paths. To avoid this, override all routing cases in the Vue application and then render a 404 page. Alternatively, if node.js is used as the background, you can use server-side routing to match urls and return 404 if no route is matched, thus implementing a Fallback.

Compare the two modes

In general, the hash mode is similar to the history mode. According to MDN, calling history.pushstate () has the following advantages over modifying the hash directly:

• pushState can be any url of the same origin as the current URL, and hash can change only the part after #, so only the url of the current document can be set

• pushState can set a new URL to be exactly the same as the current URL, which will also add the record to the stack, and the new hash value must be different to trigger the record to be added to the stack

• pushState can be added to any type of data record through the stateObject, while hash can only be added to a short string

AbstractHistory

The ‘abstract’ mode, which does not involve records associated with the browser address, is the same as ‘HashHistory’ in that it emulates the browser history stack with arrays

// Abstract. Js implementation, here through the stack data structure to simulate the routing path
export class AbstractHistory extends History {
  index: number;
  stack: Array<Route>;

  constructor (router: Router, base: ? string) {
    super(router, base)
    this.stack = []
    this.index = -1
  }
  
  // For go simulation
  go (n: number) {
    // New record location
    const targetIndex = this.index + n
    // Less than or more than exceeds returns
    if (targetIndex < 0 || targetIndex >= this.stack.length) {
      return
    }
    // Get the new route object
    // Because it is browser-independent, this must be already accessed
    const route = this.stack[targetIndex]
    // This calls confirmTransition directly
    // Instead of calling transitionTo and traversing the match logic
    this.confirmTransition(route, () = > {
      this.index = targetIndex
      this.updateRoute(route)
    })
  }

 // Confirm whether to convert the route
  confirmTransition (route: Route, onComplete: Function, onAbort? :Function) {
    const current = this.current
    const abort = err= > {
      if (isError(err)) {
        if (this.errorCbs.length) {
          this.errorCbs.forEach(cb= > { cb(err) })
        } else {
          warn(false.'uncaught error during route navigation:')
          console.error(err)
        }
      }
      onAbort && onAbort(err)
    }
    // If the two routes are the same, no operation is performed
    if (
      isSameRoute(route, current) &&
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      return abort()
    }
    // The following is the processing of various hook functions
    / / * * * * * * * * * * * * * * * * * * * * *})}Copy the code

reference

Front-end Advancements thoroughly understand front-end routing

Vue Introduction — Brief Analysis of VUE-Router