10 minutes to thoroughly understand the single page application routing

Last time, we popularized the custom routing routes of the applet and started the routing journey; Today, I'll take advantage of the trend to apply routing on a single page and chat with you for 50 cents. If you don't talk well... Return... A dollar?

Single page application features

Suppose: in a web page, there is a button. Click it to jump to other pages in the station.

Multi page application: click the button to load a new html resource and refresh the whole page;

Single page application: click the button, there is no new html request, and only local refresh occurs, which can create a near native experience, as smooth as silk.

Why can SPA single page application have almost no refresh? Because its SP -- single page. When entering the application for the first time, the only html page and its public static resources are returned. The subsequent so-called "jump" will no longer take the html file from the server, but the DOM replacement operation is simulated.

So how does js catch the opportunity of component switching and change the browser url without refreshing? Rely on hash and HTML5 history.

hash routing

features

  1. Similar to www.xiaoming.com HTML #bar is a hash route. When # the following hash value changes, it will not request data from the server. You can monitor the change of URL through hashchange event, so as to carry out DOM operation to simulate page Jump
  2. No server cooperation is required
  3. Not friendly to SEO

principle

HTML5History routing

features

  1. History mode is a new function of HTML5. It is more intuitive than hash routing. It looks like this www.xiaoming.com HTML / bar, the simulated page Jump is through history Push state (state, title, URL) to update the browser route. When the route changes, listen to the pop state event to operate the DOM
  2. The back-end cooperation is required for redirection
  3. Relatively friendly to SEO

principle

Interpretation of Vue router source code

Take Vue router of Vue as an example. Let's talk about its source code.

Tips: because the focus of this article is to explain the two modes of single page routing, only some key codes are listed below, mainly:

  1. Register plug-ins
  2. Constructor of vueroter to distinguish routing modes
  3. Global registration component
  4. push and monitoring method of hash / HTML5History mode
  5. transitionTo method

Register plug-ins

First of all, as a plug-in, you should be conscious of exposing an install method and give it to Vue's father to use.

Install JS file defines the method of registering and installing the plug-in install, blends the method into the hook function of each component, and initializes the route when the beforeCreate hook is executed:

Vue.mixin({
  beforeCreate () {
    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 {
      this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
    }
    registerInstance(this, this)
  },
  // In the full text To represent the omitted method
  ...
});

Distinguish mode

Then, we start from index JS find the base class VueRouter of the whole plug-in. It is not difficult to see that it adopts different routing instances according to different mode s in the constructor.

...
import {install} from './install';
import {HashHistory} from './history/hash';
import {HTML5History} from './history/html5';
...
export default class VueRouter {
  static install: () => void;
  constructor (options: RouterOptions = {}) {
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
           
    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}`)
      }
    }
  }
}

Register router link components globally

At this time, we may ask: when using Vue router, where are the common < router link / > and < router view / > introduced?

Go back to install JS file, which introduces and globally registers router view and router link components:

import View from './components/view';
import Link from './components/link';
...
Vue.component('RouterView', View);
Vue.component('RouterLink', Link);

Yes/ components/link.js, the < router link / > component is bound with the click event by default. Click to trigger the handler method to perform the corresponding routing operation.

const handler = e => {
  if (guardEvent(e)) {
    if (this.replace) {
      router.replace(location, noop)
    } else {
      router.push(location, noop)
    }
 }
};

As mentioned at the beginning, the vueroter constructor initializes History instances of different modes for different modes, so the router replace,router. The way of pushing is also different. Next, we pull the source code of these two modes respectively.

hash mode

History/hash. The HashHistory class is defined in the. JS file, which inherits from History / base JS.

Its prototype defines the push method: in the browser environment that supports HTML5 history mode (supportsPushState is true), call history Pushstate to change the browser address; In other browser environments, you will directly use location Hash = path to replace with a new hash address.

In fact, there are some questions here at the beginning. Since it is already a hash mode, why judge the supportsPushState? To support scrollBehavior ´╝îhistory.pushState can pass the parameter key to the past, so that each url history has a key, and the key is used to save the location information of each route.

At the same time, the setupListeners method bound on the prototype is responsible for monitoring the timing of hash change: in the browser environment supporting HTML5 history mode, listen for the pop state event; In other browsers, it listens for hashchange. After listening to the change, trigger the handleRoutingEvent method and call the transitionTo jump logic of the parent class to replace the DOM.

import { pushState, replaceState, supportsPushState } from '../util/push-state'
...
export class HashHistory extends History {
  setupListeners () {
    ...
    const handleRoutingEvent = () => {
        const current = this.current
        if (!ensureSlash()) {
          return
        }
        // The jump method under the parent class History called by transitionTo. After the jump, the path will be hash ed
        this.transitionTo(getHash(), route => {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      }
      const eventType = supportsPushState ? 'popstate' : 'hashchange'
      window.addEventListener(
        eventType,
        handleRoutingEvent
      )
      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
    )
  }
}
...

// Process the URL with the incoming path in the form of hash
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}
...

// Replace hash
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

// util/push-state. Methods in JS file
export const supportsPushState =
  inBrowser &&
  (function () {
    const ua = window.navigator.userAgent

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

HTML5History mode

Similarly, the HTML5History class is defined in history / HTML5 JS.

Define the push prototype method and call history Pushstate modifies the path of the browser.

At the same time, the prototype setupListeners method listens to the event of pop state and makes DOM replacement in time.

import {pushState, replaceState, supportsPushState} from '../util/push-state';
...
export class HTML5History extends History {

  setupListeners () {

    const handleRoutingEvent = () => {
    const current = this.current;
    const location = getLocation(this.base);
    if (this.current === START && location === this._startLocation) {
      return
    }

    this.transitionTo(location, route => {
      if (supportsScroll) {
        handleScroll(router, route, current, true)
      }
    })
    }
    window.addEventListener('popstate', handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }
}

...

// util/push-state. Methods in JS file
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  const history = window.history
  try {
    if (replace) {
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

transitionTo handles routing change logic

The two routing modes mentioned above trigger this when listening Tooniti, what is this? It is actually defined in history / base The prototype method on the JS base class is used to handle the routing change logic.
First pass const route = this router. Match (location, this. Current) compares the incoming value with the current value and returns the corresponding routing object; Then judge whether the new route is the same as the current route, and return directly if it is the same; Different, then in this Execute the callback in confirmtransition to update the routing object and replace the DOM related to the view.

export class History {
 ...
 transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    const route = this.router.match(location, this.current)
    this.confirmTransition(
      route,
      () => {
        const prev = this.current
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })

        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          this.ready = true
          // https://github.com/vuejs/vue-router/issues/3225
          if (!isRouterError(err, NavigationFailureType.redirected)) {
            this.readyErrorCbs.forEach(cb => {
              cb(err)
            })
          } else {
            this.readyCbs.forEach(cb => {
              cb(route)
            })
          }
        }
      }
    )
  }
  ...
}

last

Well, the above is some small knowledge of single page routing. I hope we can get started and never give up together~~

Tags: Javascript Front-end Vue.js Back-end Mini Program

Posted by trellie on Wed, 25 May 2022 17:27:57 +0300