Front-end routing evolution: from multi-page applications to single-page applications

Multi-page application: the beginning of the web page is multi-page, a complete web application has multiple complete HTML composition, through the A tag corresponding to different URLS, the server side to return different pages according to the URL, those pages in the server side are real existence.

Front end can do at this time is very limited, front and back side is not completely separate, until the advent of Ajax front end to be able to do more complicated things, before and after the job is more and more clear, with the constant development of the business process, due to the front-end project is becoming more and more complicated, so we want to consider to split out the front application part, Make it an app that can be developed and run independently, rather than relying on a backend rendering of HTML for multiple pages. Single-page applications have emerged.

As the name implies, a WEB project has only one HTML page. Once the page is loaded, SPA will not reload or jump the page due to the user’s operation. Instead, JS is used to dynamically transform HTML content to simulate multiple view jumps.

In the final analysis, routing is to display different contents or pages according to different urls, and the essence of routing is to establish a mapping relationship between URLS and pages.

The process of sending a request to a rendered page:

Multi-page applications use back-end control routing, that is, back-end routing:

Single-page applications use front-end control routing, that is, front-end routing:

Implementation principles of front-end routing

One of the core aspects of a single page application (SPA) is updating the view without rerequesting the page.

To put it simply, it is to match a special URL for each display form of view in SPA while ensuring only one HTML page and not refreshing or skipping pages when interacting with the user. Refresh, advance, retreat and SEO are achieved through this special URL.

To achieve this goal, we need to do the following two things:

  1. Change the URL and prevent the browser from sending requests to the server
  2. You can listen for URL changes.

To do this, browsers provide hash and History modes.

Two routing modes

hash

Hash refers to the content that appears after the # sign. Although it appears in the URL, it is not included in the HTTP request and has no impact on the back end, so changing the hash does not reload the page. Hash changes trigger the Hashchange event, which can also be controlled by the browser’s forward and backward movements. Therefore, before the emergence of h5’s History mode, hash mode was basically used to implement front-end routing.

The relevant API

Window.location. hash = 'hash string '; // Set hash value let hash = window.location.hash; // Get current hash value // listen for hash change, Window.addeventlistener ('hashchange', function(event){let newURL = event.newurl; // hash the new url let oldURL = event.oldurl; // Hash the old url},false)Copy the code
history

Browsers had history objects before HTML5. But in the early days of History, it could only be used for multiple page jumps, just as in the early days of routing, we always used the following API to control page jumps.

history.go(-1); // go(2); // forward two pages history.forward(); // forward a page history.back(); // Go back one pageCopy the code

In the HTML5 specification, History adds the following apis:

history.pushState(); // Add a new state to the history stack history.replacEstate (); // Replace the current state with the new state history.state // return the current state objectCopy the code

For details about the History API, see MDN

Because history.pushState() and history.replacestate () can change urls without refreshing the page, the browser does not immediately send requests to the back end. So histroy in HTML5 has the ability to implement front-end routing.

So how do you listen for this url change? You need to understand the ONPopState API

Backend configuration support

History mode routing is usually used in single-page applications and requires background configuration support. Because in a single-page client application, if the background is not properly configured, when the user accesses oursite.com/user/id directly from the browser, the server will retrieve the file by default and return 404 if it fails to retrieve it, which is not what we want. 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 the app relies on.

// nginx example location / {try_files $uri $uri/ /index.html; }Copy the code

To sum up, both hash and History modes can change routes without sending a request to the server, and can listen for route changes through the response API. Both modes are capable of implementing front-end routing. The main differences are as follows:

Routing patterns The current routing Set the routing Listening for Route changes
hash location.hash location.hash = ‘aaa’ onhashchange
history location.pathname pushState/replaceState onpopstate

Vue – the router example

Modern front-end projects are mostly single page Web applications (SPA), in which route is an important link. Every modern front-end framework has a routing implementation corresponding to it, such as vue-Router, React-Router, etc.

Starting with a vue-Router example application, this paper introduces the basic implementation principle and method of front-end routing.

Vue-router is the official router of vue. js, which is deeply integrated with the vue. js kernel, making it easy to build single-page applications with vue. js.

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.

  1. Create a simple VUE projectvue create vuerou
  2. Download NPM I vue-router-s
  3. Write the router. Js
import Vue from 'vue'; VueRouter import VueRouter from 'vue-router'; import Home from '.. /views/Home. Vue '// 2. Let router = new VueRouter({routes: [{path: '/home', Component: home}]}); // 4. Export the route object export default RouterCopy the code
  1. The main js, the introduction of the route
import Vue from 'vue' import App from './App.vue' // 5. Import router from './router' //new Vue Router render: h => h(App)}).$mount('# App ')Copy the code
  1. Write the router-view tag to app.vue
//app.vue <template> <div id="app"> <router-view/> </div> </template>Copy the code

At this point, the simplest vue-Router example project is complete.

Implement your own vue-Router plug-in

Analysis of vue-Router plug-in principle:

Create a myRouter folder, create an index.js file, and implement vue-Router logic here.

  1. Create a new vueRouter class. Implement the createMap method, using a map object to map path to Component. The results of the createMap method are stored on the routesMap property.
class vueRouter{ constructor(options){ this.mode=options.mode || 'hash'; Thinking this. / / data structure routes = options. The routes | | []; this.routesMap = this.createMap(this.routes); } createMap(routes){ return routes.reduce((memo, current)=>{ memo[current.path]=current.component; return memo; }, {}); }}Copy the code
  1. Class vueRouter adds a history attribute that points to the current route, the first time the route is loaded, and the value of history that needs to be updated every time the route changes. Combine the hash analyzed above with the current value of history.
class HistoryRouter{ constructor(){ this.current=null; } } class vueRouter{ constructor(options){ this.mode=options.mode || 'hash'; Thinking this. / / data structure routes = options. The routes | | []; this.routesMap = this.createMap(this.routes); this.history = new HistoryRouter(); } createMap(routes){ return routes.reduce((memo, current)=>{ memo[current.path]=current.component; return memo; }, {}); } init(){ if(this.mode === 'hash'){ location.hash? '':location.hash='/'; window.addEventListener('load', ()=>{ this.history.current=location.hash.slice(1); }) window.addEventListener('hashchange', ()=>{ this.history.current = location.hash.slice(1); }) } else { location.pathname? '':location.pathname = "/"; window.addEventListener('load',()=>{ this.history.current = location.pathname }) window.addEventListener("popstate",()=>{ this.history.current = location.pathname }) } } }Copy the code

This completes the first three steps of the flowchart, namely:

Next you need to bind the current changes to the view. That is, you need to listen for the current variable and update the corresponding view when the current variable changes. The util function defineReactive can be used to support bidirectional binding. If the principle of bidirectional binding is not clear, you can refer to my other article to understand the vUE source code, from 0 to 1 to achieve their own MVVM framework. Of course, children who are familiar with the principle of bidirectional binding can also implement their own bidirectional binding function.

It is also important to note that vueRouter functions as a plugin for Vue, so vueRouter must provide an install function to register the plugin. For those unfamiliar with the plugin mechanism of Vue, please refer to the official documentation.

  1. VueRouter performs bidirectional binding of Current through global blending of the injected component options throughout the lifecycle of the component. Two read-only attributes are exposed to facilitate the operation of the router

At this point, the code looks like this:

VueRouter. Install =function(vue){// singleton mode. Keep only one global object, consider the code robustness if (vueRouter. Install. Installed) return; vueRouter.install.installed=true; Vue. Mixin ({beforeCreate:function() {// main -> app.vue -> component if(this.$options && this.$options. this._root=this; this._router=this.$options.router; vue.util.defineReactive(this, 'current', this._router.history); }else if(this.$parent){// app.vue mount parent _root // console.log(this) this._root=this. DefineProperty (this,'$router', {get(){return this._root._router; }}); Object.defineProperty(this,'$route', { get(){ return this._root._router.history.current; }})},})}Copy the code

This is only half of the story. We have bidirectional binding for the current variable. The bidirectional binding mechanism in Vue triggers view updates when the current variable changes, i.e. the el host node is rerendered. Open app.vue and check the router-view component corresponding to #app node of EL, which is used to render the components matched by the highest-level route.

  1. So we’ll implement the router-view component in our custom implementation vuerOuter as well.
Vue.com ponent('router-view',{render ('router-view')) {render ('router-view',{ render(h){ let current = this._self._root._router.history.current; let routesMap = this._self._root._router.routesMap; return h(routesMap[current]); }})Copy the code

Check out the Vue-Router API for another frequently used component that provides a-tag hyperlink jumps for jumping between different view components.

  1. Extension of router-link component functionality in custom implemented Vuerouter
vue.component('router-link',{ props:{ to:String }, render(h){ let mode = this._self._root._router.mode; let to = mode === "hash"?" # "+ enclosing, enclosing the to return h (' a ', {attrs: {href: to}}, enclosing $slots. The default) / / return h (' a ', {}, 'front page')}})Copy the code

At this point, the vuerouter code with the most basic functionality is in place. The complete code is as follows:

class HistoryRouter{ constructor(){ this.current=null; } } class vueRouter{ constructor(options){ this.mode=options.mode || 'hash'; Thinking this. / / data structure routes = options. The routes | | []; this.routesMap = this.createMap(this.routes); this.history = new HistoryRouter(); this.init(); this.afterEach=()=>{}; } init(){ if(this.mode === 'hash'){ location.hash? '':location.hash='/'; window.addEventListener('load', ()=>{ this.history.current=location.hash.slice(1); }) window.addEventListener('hashchange', ()=>{ this.history.current = location.hash.slice(1); }) }else{ location.pathname? '':location.pathname = "/"; window.addEventListener('load',()=>{ this.history.current = location.pathname }) window.addEventListener("popstate",()=>{ this.history.current = location.pathname }) } } createMap(routes){ return routes.reduce((memo, current)=>{ memo[current.path]=current.component; return memo; }, {}); }} vueRouter. Install =function(vue){// singleton mode. Keep only one global object, consider the code robustness if (vueRouter. Install. Installed) return; vueRouter.install.installed=true; Vue. Mixin ({beforeCreate:function() {// main -> app.vue -> component if(this.$options && this.$options. this._root=this; this._router=this.$options.router; vue.util.defineReactive(this, 'current', this._router.history); }else if(this.$parent){// app.vue mount parent _root // console.log(this) this._root=this. DefineProperty (this,'$router', {get(){return this._root._router; }}); Object.defineProperty(this,'$route', { get(){ return this._root._router.history.current; }})}, }) vue.component('router-view',{});} render(h){ let current = this._self._root._router.history.current; let routesMap = this._self._root._router.routesMap; return h(routesMap[current]); } }) vue.component('router-link',{ props:{ to:String }, render(h){ let mode = this._self._root._router.mode; let to = mode === "hash"?" # "+ enclosing, enclosing the to return h (' a ', {attrs: {href: to}}, enclosing $slots. The default) / / return h (' a ', {}, 'front page')}})} export default vueRouterCopy the code
  1. Test whether the MyRouter plug-in is working

Compile the router configuration file in the vuerou folder and replace the statement importing the vueRouter plug-in with import vueRouter from ‘.. / myRouter ‘, re-run the project to check that the route has taken effect.

Write in the last

This article provides a basic framework and ideas for understanding the vueRouter source code. You will need to have a deep understanding of the vue ecosystem, such as vue plug-in mechanism, Vue two-way binding principle, back-end and front-end routing, hash mode API and History mode API.

For easy reading, the code in this article has hosted Gitee

If there are any mistakes in this article, please correct them in the comments section. If this article helps you, please like it and follow it