Recently, I was asked a question: do you use Vue SPA (single page) or Vue multi-page design in your project?

This article focuses on the SPA single page design of Vue. Click here to see how to expand Vue multi-page design.

What is vue-router?

First we need to know what vue-Router is, what does it do?

I’m not talking about a hardware router. I’m talking about a SPA (single page application) path manager. In other words, vue-Router is the link path management system of WebApp.

Vue-router is the official routing plug-in of vue.js. It is deeply integrated with vue.js and suitable for building single-page applications.

What’s the difference between that and traditional page hopping?

1. The single-page application of VUE is based on routing and components. Routing is used to set access paths and map paths to components.

2. In traditional page applications, some hyperlinks are used to realize page switching and skipping.

In vue-Router single-page applications, it is switching between paths, that is, switching between components. The essence of routing module is to establish the mapping between URL and page.

The reason why you can’t use a tag is because Vue is a single-page application that has only one main index.html page, so the tag you write is useless and must be managed using vue-Router.

Vue-router implementation principle

SPA(Single Page Application): a single page application with only one complete page; When it loads a page, it does not load the entire page, but only updates the contents of a specified container.

One of the core aspects of a single page application (SPA) is:

1. Update the view without rerequesting the page;

2. Vue-router provides three modes to implement single-page front-end routing: Hash mode, History mode, and Abstract mode. Determine which mode to adopt according to the mode parameter.

Routing patterns

Vue-router provides three operating modes:

● Hash: Uses the URL hash value for routing. Default mode.

● History: Rely on the HTML5 History API and server configuration. Check out the HTML5 History mode.

● Abstract: Support all JavaScript runtime environments, such as node.js server.

Hash pattern

Vue-router default mode is hash mode — the hash of a URL is used to simulate a complete URL, and the page is not reloaded when the URL changes.

Hash (#) is the anchor point of the URL, which represents a location in the web page, changing only the part after the # (/#/..). , the browser will only load the content of the corresponding position, but will not reload the page, that is, the # is used to guide the browser action, completely useless on the server side, HTTP request does not include #; At the same time, every time the part after # is changed, a record will be added to the browser’s access history. Use the “back” button to go back to the previous position. So Hash mode renders different data for the specified DOM position by changing the anchor value.

The History mode

The HTML5 History API provides a way for developers to change the URL of a site without refreshing the entire page by using the history.pushState API to redirect urls without reloading the page.

Since hash mode has a hash tag attached to the URL, if you don’t want an ugly hash, you can use the history mode of the route. Just add “mode: ‘history'”, which makes full use of the history.pushState API to redirect urls without reloading the page.

// const router = new VueRouter({mode:'history',
  routes: [...]
})
Copy the code

When using history mode, urls like normal urls, such as yoursite.com/user/id, are better… However, this mode to play well, but also need background configuration support. Because our application is a single-page client application, if the background is not properly configured, when the user accesses directly in the browser

So, you 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.

export const routes = [ 
  {path: "/", name: "homeLink", component:Home}
  {path: "/register", name: "registerLink", component: Register},
  {path: "/login", name: "loginLink", component: Login},
  {path: "*", redirect: "/"}]
  
Copy the code

This is set up to automatically jump to the Home page if the URL is typed incorrectly or does not match any static resources.

The abstract model

The Abstract pattern uses a browser-independent browser history virtual management back end.

Based on platform differences, only the Abstract mode is supported in Weex environments. However, vue-router validates the environment. If the browser API is not available, vue-Router forces the abstract mode automatically. Therefore, when using vue-Router, you only need to leave the mode configuration blank. By default, hash mode is used in the browser environment and Abstract mode is used in the mobile native environment. (Of course, you can explicitly specify to use abstract mode in all cases.)

Vue-router Indicates the application mode

1: Download NPM I vue-router-s

** import VueRouter from ‘vue-router’ in main.js;

3: Install vue.use (VueRouter);

4: Creates a routing object and configures routing rules

let router = new VueRouter({routes:[{path:’/home’,component:Home}]});

5: Pass its routing object to the instance of Vue. Add router: Router to options

6: Leave a pit in app.vue

<router-view></router-view>
Copy the code

See the following code for concrete implementation:

// import Vue from from main.js'vue';
import VueRouter from 'vue-router'; // Main import App from'./components/app.vue';
import index from './components/index.vue'Vue.use(VueRouter); // Create a routing object and configure routing rulesletRouter = new VueRouter({routes: [//'/index', component: index } ] }); // start new Vue({el:'#app'Router: router, // Render router: c => c(App),})Copy the code

Finally remember to “leave a pit” in app.vue

//app.vue <template> <div> <! <router-view></router ></ div> </template> <script>export default {
        data() {return {}
        }
    }
</script>
Copy the code

Vue-router source code analysis

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 = [] Enclosing afterHooks = [] / / create the matcher matching function enclosing the matcher = createMatcher (options. The routes | | []. This) // instantiate specific History according to mode, default is'hash'modellet mode = options.mode || 'hash'// Check whether the browser supports supportsPushState'history'Mode // If set to'history'But if the browser doesn't support it,'history'The mode will fall back to'hash'The // fallback mode controls whether the route should be rolled back to when the browser does not support history.pushStatehashMode. The default value istrue. this.fallback = mode ==='history'&&! supportsPushState && options.fallback ! = =false
    if (this.fallback) {
      mode = 'hash'} // If it is not inside the browser, it will become'abstract'modelif (!inBrowser) {
      mode = 'abstract'} this.mode = mode // Select and instantiate the corresponding History class 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 {returnthis.matcher.match(raw, current, redirectedFrom) } get currentRoute (): ? Route {returnthis.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 // according tohistoryPerforms the corresponding initialization operations and listensif (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (historyinstanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, SetupHashListener)} history.listen(route => {this.apps.forEach((app) => {app._route = route})})} // Before route jump beforeEach (fn: Function): Function {returnRegisterHook (this.beforeHooks, fn)} // Route navigation is confirmed between front beforeResolve (fn: Function): Function {returnAfterEach (fn: Function): Function {afterEach (fn: Function): Function {returnRegisterHook (this.afterhooks, fn)} // The callback Function called when the first route jump is complete onReady (cb: Function, errorCb? : Function) {this.history.onReady(cb, errorCb)} Function) {this.history.onError(errorCb)} // Add a routehistoryStack adds a record, and clicking back returns you to the previous page. push (location: RawLocation, onComplete? : Function, onAbort? : Function) {this.history.push(location, onComplete, onAbort)historyAdd a new record inside, click back, will jump 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 forward or backward from the current page, similar to window.history.go(n). N can be positive or negative. Go (n: number) {this.history.go(n)} // Go back to the previous pageback() {this.go(-1)} // proceed to the next pageforward() { this.go(1) } getMatchedComponents (to? : RawLocation | Route): Array<any> { const route: any = to ? to.matched ? to : this.resolve(to).route : this.currentRouteif(! 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.

exportclass HashHistory extends History { constructor (router: Router, base: ? string, fallback: boolean) { super(router, base) // checkhistoryFallback deeplinkinghistoryMode is degraded. Need to do a degrade checkif(fallback && checkFallback(this.base)) {// If degraded and degraded, returnreturn
    }
    ensureSlash()
  }
  .......
Copy the code
functionCheckFallback (base) {const location = getLocation(base) // Get the real location value except baseif(! / ^ \ /#/.test(location)) {// If the address does not start with /# at the beginning of// We need to do a downgrade tohashIn the mode should be /# at the beginning
    window.location.replace(
      cleanPath(base + '/ #' + location)
    )
    return true}}functionEnsureSlash (): Boolean {// gethashValue const path = getHash()if (path.charAt(0) === '/') {// If it starts with a /, just return itreturn true} // If not, you need to manually ensure a replacementhashValue 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! // Window.location. hash is not directly used here due to compatibility issues // Due to Firefox decodehashValue const href = window.location.href const index = href.The '#')
  return index === -1 ? ' 'DecodeURI (href. Slice (index + 1))hashThe previous URL addressfunction 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 onehash
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else{window.location.hash = path}} // substitutehash
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) () => {// confirm whether to convert this.updateroute (route) // updateRoute onComplete && onComplete(route) this.ensureurl () // fire ready CBS onceif(! this.ready) { this.ready =true
        this.readyCbs.forEach(cb => { cb(route) })
      }
    }, err => {
      if (onAbort) {
        onAbort(err)
      }
      if(err && ! this.ready) { this.ready =trueEnclosing readyErrorCbs. ForEach (cb = > {cb (err)})}})} / / routing update updateRoute (route: This.cb && this.cb(Route) {const prev = this.current = Route This callback function is registered in the index file could update data on the hijacked _router enclosing the router. AfterHooks. ForEach (hook = > {hook && hook (route, prev)})}}Copy the code

pushState

export functionpushState (url? : string, replace? : boolean) { saveScrollPosition() // try... catch the pushState call to get around Safari // DOM Exception 18whereIt limits to 100 pushState calls // Catch is because Safari has a limit of 100 calls to pushState, which throws DOM Exception 18 consthistory = window.history
  try {
    ifReplaceState ({replace: _key}) {// replace: key is the current key, no need to generate new history.replaceState({key: _key},' ', url)
    } else{// create key _key = genKey() // create new key history.pushState({key: _key},' '}} catch (e) {// if the limit is reached, the new address window.location[replace?'replace' : 'assign'](url)
  }
}
Copy the code

replaceState

// Call pushState directly to replace astrue
export functionreplaceState (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.

exportclass HTML5History extends History { constructor (router: Router, base: ? string) { super(router, Base) const expectScroll = the router. The options. ScrollBehavior / / rollback const manner supportsScroll = supportsPushState && expectScrollif(supportsScroll) {setupScroll()} const initLocation = getLocation(this.base) // Monitor popState window.adDeventListener ('popstate', e => {
      const current = this.current

      // Avoiding first `popstate` event dispatched in some browsers but first
      // historyRoute not updated since async guard at the same time. // Avoid the first "popState" event in some browsers.historyRoutes are not updated 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 http://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 pathexportclass AbstractHistory extends History { index: number; stack: Array<Route>; constructor (router: Router, base: ? String) {super(router, base) this.stack = [] this.index = -1} Number) {// New historical location const targetIndex = this.index + n // returns if less than or greater thanif (targetIndex < 0 || targetIndex >= this.stack.length) {
      return} // make a route object that is already const route = this.stack[targetIndex] // make a route object that is already const route = this.stack[targetIndex] // This. ConfirmTransition (route, () => { this.index = targetIndex this.updateRoute(route) }) }Copy the code
// Confirm whether to convert 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 previous route is the same, no operation is performedif (
      isSameRoute(route, current) &&
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      returnAbort ()} / / here are all kinds of hook function of processing / / * * * * * * * * * * * * * * * * * * * * *})}Copy the code

See here you have a basic command of vue-Router routing, if you like to see the source can click to view

If you like, you can give me a star, Github

Thanks to Aine jie and CaiBoBo for their ideas.