In order to keep pace with the rapid development of single page Web applications, various front-end component technology stacks emerge in an endless stream in recent years. React and Vue stand out through constant version iteration and become the two most popular technology stacks at present.

In just seven months, both stacks have been downloaded millions of times, and React has even been downloaded millions of times. From React and Vue to Angular and Ember, single-page Web applications rely on front-end routing. If the single-page Web application is compared to a room, each page corresponds to each room in the house, then routing is the door of the room, no matter how beautiful the room is decorated, there is no door, nor can it be displayed in front of the user, the status of routing in the single-page Web application is self-evident.

In order to introduce front-end routing in more detail, xiaobian will lead you to explore the implementation principle of front-end routing step by step from three aspects, from shallow to deep. First, learn the core knowledge of front-end routing through the Core Principles of SPA Routing Trilogy. Then, the MyVueRouter Practice of SPA Routing Trilogy will guide you to realize your own Vue-Router. Finally “SPA Routing trilogy VueRouter source parsing” will challenge myself, in-depth analysis vue-Router source code. The Core Principles of SPA Routing Trilogy gives a preliminary understanding of front-end routing from three parts: the past and present of end routing, the analysis of core principles, and the comparison of vue-Router and React-Router applications.

Front-end routing has a history

The development of front-end routing has gone through the process of back-end routing, front-end and back-end routing transition, and front-end routing. If you still don’t understand front-end routing, it is necessary to understand its development process.

The back-end routing

The concept of routing first appeared on the back end, back when Web development was still in the slash-and-burn era, back-end routing was dominant and page rendering was completely dependent on the server.

At the beginning, FILES such as HTML, CSS, JavaScript and data carrier JSON (XML) were put in the directory of the back-end server, and these files were unrelated to each other. If you want to change the layout of a website, you may have to change hundreds of HTML, which is tedious and has no technical content. Clever engineers then reformatted the same HTML into templates, successfully reducing the front-end workload. Front-end engineers started using template language instead of handwritten HTML, and the files in the back-end server directory became different template files.

At this time, no matter what language framework the Web backend was, there would be a dedicated routing module or routing area to match the URL address given by the user, as well as some form submission and page request address. When the user switches the page, the browser sends different URL requests. When the server receives the browser’s requests, it parses different URL addresses for back-end routing matching. After stitching the template together, it returns it to the front-end complete HTML. Also known as server-side rendering.

The server side renders the page, and the back end has a complete HTML page, making it easier for crawlers to obtain information, which is conducive to SEO optimization. It consumes less resources for the client, especially for the mobile terminal, which can save power.

The transition

Web applications developed on the basis of back-end routing have a disadvantage. Each jump to a different URL is a re-visit to the server, the server spliced together to form a complete HTML, back to the browser, the browser to render the page. Even the browser’s forward and back keys revisit the server, not using the cache properly.

As the front-end pages become more complex and more functional, the code files in the back-end server directory become more and more coupled. Not only increase the pressure on the server, but also not conducive to good user experience, code maintenance. Limited by the rise of front-end technology represented by JavaScript, this pain point has become the biggest problem for programmers.

It wasn’t until 1998 that the Outloook Web App team at Microsoft came up with the basic concept of Ajax (the predecessor of XMLHttpRequest), which I’m sure you’re already familiar with. It’s a technical solution for browsers to implement asynchronous loading. This technology is implemented in Internet Explorer 5 via ActiveX. With Ajax, you don’t have to refresh the page every time you do something on a page, and the experience is vastly improved.

The release of Google Maps in 2005 took Ajax to the next level, demonstrating its true power beyond simple data and page interactions, and laying the foundation for asynchronous interactive experiences to flourish. With the launch of Google’s V8 engine in 2008 and the rise of JavaScript, front-end engineers began borrowing ideas from back-end templates, and single-page applications were born. When Google released Angularjs in 2009, they took MVVM and single-page apps to the next level.

The single-page application is not only refreshed in page interaction, but also refreshed in page jump. In order to realize the single-page application jump, front-end routing is born.

The front-end routing

Compared with the back-end route, the front-end route performs view switching without completely refreshing the page. The page URL has changed, but it hasn’t been reloaded, making the user experience more similar to the native app.

With the rise of front-end routing, page rendering has changed from server rendering to front-end rendering. Why do you say that? To request a URL, the server doesn’t need to concatenate a template. Instead, the server returns an HTML file, which looks something like this:

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Demo</title> <link href="app.css" rel="stylesheet"/>  </head> <body> <div id="app"></div> <script type="text/javascript" src="app.js"></script> </body> </html>Copy the code

All that’s left is a <div id=”app”></div> and a bunch of JS files, so the HTML is incomplete. The page we see is rendered through this series of JS, which is called front-end rendering. Front-end rendering solves the problem of page construction through the computing power of the client, which greatly relieves the pressure of the server.

Single page development is the trend, but we can’t ignore the shortcomings of front-end rendering. As the server does not retain the complete HTML, dynamic DOM splicing through JS takes extra time, which is not as fast as server-side rendering and is not conducive to SEO optimization. Therefore, in real development, you should not blindly choose the rendering method, it must be based on the business scene. For sites with no complex interactions and strict SEO requirements, server rendering is also the right choice.

Analysis of core principles

The route describes the mapping between the URL and the UI. The mapping is one-way, that is, the URL changes cause the UI to update (without refreshing the page). Front-end routes are mainly displayed in the following two modes:

  • Hash front-end routing: The URL in the address bar has the hash value, which is ugly but highly compatible.
  • Front-end routing without hash: No # in the URL of the address bar, nice, but some browsers do not support it, and also requires back-end server support.

In vue-router and React – Router, the two display modes are defined into Hash mode and History mode. The implementation principle of front-end routing is very simple. In essence, it detects URL changes, intercepts URL addresses, and implements UI updates by parsing and matching routing rules. Now follow xiaobian together to uncover its mysterious veil!

Hash

A complete URL includes: protocol, domain name, port, virtual directory, filename, parameter, anchor.

The hash value refers to the anchor part of the URL, the part after the #. Hash, also known as anchor points, is used for page positioning, and the DOM ID corresponding to the hash value is displayed in the visual area. Before the advent of HTML5’s History feature, front-end routing was mostly implemented using listening hash values. The hash update has the following features:

  • The hash value is the flag bit of the web page. The HTTP request does not contain the anchor part and has no impact on the backend
  • Because the HTTP request does not contain an anchor part, web page reloads are not triggered when the hash value changes
  • Changing the hash value alters the browser’s history
  • Changing the hash value triggers the window.onHashchange () event

There are three ways to change the hash value:

  • By setting the value of window.location.hash
  • Browser forward (history.forword()), back(history.back())

In summary, these three ways of changing the hash value do not cause the browser to send a request to the server, and the browser will not refresh the page if it does not send a request. The hash value changes, triggering the hashchange event on the global Window object. Therefore, hash-mode routing uses hashchange event to monitor changes in URL and DOM operations to simulate page jumps.

History

Before I go into History, let me ask you a question: why does clicking on the back button in the upper left corner of the browser go back to the previous page, and clicking on the forward button go back to the previous page? This is because browsers have a stack-like history that follows a first in, last out rule. Every URL change, including a hash value change, creates a history in the browser. The Window object provides access to the viewer’s history through the history object.

  • History.length For security reasons, the history object does not allow unauthorized code to access URLs from other pages in the history, but the length of the history object can be accessed through history.length.
  • History.back () rewind to the previous history, same as the browser back key
  • History.forward () advances to the next history, same as the browser forward key
  • History.go (n) jumps to the corresponding access record; If n > 0, then forward, if n < 0, then backward, if n = 0, then refresh the current page

To accommodate the growth of single pages, HTML5 has added two new methods to the History API: pushState() and replaceState(), both with the ability to manipulate browser History.

history.pushState(state, title, URL)

PushState accepts three parameters:

  • State: Stores the state object corresponding to the URL, which can be obtained from history.state
  • Title: title, currently not supported by browsers
  • URL: Defines a new historical URL record. Note that the new URL must be of the same origin as the current URL and cannot cross domains

PushState adds a entry to the browser’s history. History. length is +1, and the current browser URL becomes the new ONE. Note that simply changing the browser URL to a new URL does not load or refresh the page. Here’s a quick example:

Through the history. PushState ({tag: “Cart”}, “”, “cart.html”), only the URL is changed when /home/html is changed to /cart.html, the cart.html page is not loaded, and the browser does not even check if the path exists. This proves that pushState modifies browser URLS without refreshing the page, and single-page routing takes advantage of this feature.

If you are careful, you will notice that setting a new URL via pushState is similar to setting a HASH value to change the URL via window.location=’#cart’ : Urls are changed, new history entries are created and activated within the current document, but the page is not re-rendered, and the browser does not initiate a request. What are the advantages of the former?

  • The new URL can be any homologous URL, whereas window.location can only be kept in the current document by changing the hash value, and the browser does not request it
  • The new URL can be the current URL. If it does not change, a new history entry can be created. Window. location must set a different hash value to create a new history entry. Suppose the current URL is/home.html#fooIs used to set the hash using window.location

The value must not be #foo in order to create a new history

  • You can add any data to a new history entry with the state argument, while changing the hash with window.location only converts the relevant data into a short string that is placed after the hash value in the form of query
  • Although the title parameter is not yet supported by all browsers, the front end is evolving so fast that who knows what will happen next!

history.replaceState(state, title, URL)

The use of replaceState is very similar to pushState, changing the current URL without refreshing the page. The difference is that replaceState modifies the current history entry rather than creating a new one, and the value of history.length remains the same.

From the animation above, we can see that history.replaceState({tag: “Cart”}, “”, “” cart. HTML “), “/classify. HTML /home.html “,” /home. /home/home.html was skipped. This proves that replaceState changes the history of /home/html to /cart.html instead of creating a new /cart.html.

window.onpopstate()

The window. Onload event is triggered when a page is jumped from a tag or window.location, and the page is rendered. Clicking the browser’s back or forward key also reloads (in Chrome) or leaves the previous page (in Safari), depending on the browser’s mechanism. PushState () or history.replacestate () does not respond to clicking the browser’s back or forward key, so how do you control page rendering? To match history.pushState() or history.replacestate (), HTML5 also adds an event to listen for URL history changes: window.onpopState ().

The official description of the window.onpopState () event looks like this:

Whenever the active history entry changes, the popState event is fired on the corresponding Window object. If the currently active history entry was created by the history.pushState() method or modified by the history.replacestate () method, The state property of the POPState event object contains a copy of the state object for this history entry. Calling history.pushState() or history.replacEstate () does not trigger the popState event. The popState event is only triggered when the browser does something, such as clicking the back or forward button (or calling the history.back(), history.forward(), or history.go() methods in JavaScript). The anchor point of the A tag also triggers this event.

When I first read this, I didn’t understand it. After thinking about it for a long time and doing a lot of examples, I found a lot of bugs, mostly because each browser has a different mechanic. Window.onpopstate () has very little description in the official documentation, and there are a lot of unclear places.

1. Whenever an active history entry changes, the PopState event is emitted on the corresponding Window object.

Enter a URL into the browser and make it active. Either way, popState will be triggered whenever the URL changes. But here’s what happened: A URL that has been changed by pushState or replaceState will only be triggered when the browser back button is pressed. A URL that has been changed by a TAB or window.location will only be triggered when the browser back button is pressed. It doesn’t trigger. I have two guesses about this scenario:

  • The popState event is an asynchronous function. Because when the URL is changed via the A tag or window.location, the current page is unloaded and the new page is loaded. Because the PopState event is asynchronous, it does not load before the page is unloaded.
  • The popState event will only be triggered if the new pushState and replaceState history entries are triggered. After all, the PopState event occurs in conjunction with pushState and replaceState.

After reviewing a lot of data, these two guesses are not confirmed, but one thing is certain: to listen for popState events, you must use pushState and replaceState changed histories.

2. Calls to history.pushState() or history.replacEstate () do not trigger popState events. Popstate events are only triggered by certain browser actions.

Due to the different mechanisms of each browser, the test results are also different. Let’s start with a test in Chrome: home.html

<div>
  <h3>home html</h3>
  <div id="btn" class="btn">跳转至 cart.html</div>
  <a href="classify.html"> a 标签跳转至 classify.html</a>
</div>
<script>
  document.getElementById('btn').addEventListener('click', function(){
       history.replaceState({ tag: "cart" }, "", "cart.html")
   }, false); 
   window.addEventListener('popstate', ()=>{
      console.log('popstate home 跳转')
   })
</script>
Copy the code

PushState ({tag: “cart”}, “”, “cart. HTML “) changes the current URL to /cart. During this process, the popState event in home.html does not fire. When you hit the browser back button, the URL changes back to /home/html, and the popState event in home.html is raised.

What if we jump out of the document in /home/html? After history.pushState({tag: “cart”}, “”, “cart.html”) changes the current URL to /cart.html, click the A tag to change the URL to /classify.

At this point, we need to be clear: the A tag changes the URL, the browser reinitiates the request, the page jumps, and the Window object changes. The popState event is triggered on the corresponding window object. At this point, we hit the browser back button, the URL changes to /cart.html, the load event in /cart.html is executed, and the page loads. Click the browser back button again, the URL changes to /home.html, the popState event in /cart.html is triggered, and the page is not rendered.

The popState event is triggered, but it is a popState event defined in the Cart.html page, not home.html. And the same browser backclick operation, shown in Safari, looks like this:

During browser rollback, Safari and Chrome load pages differently. [false] [false] [false] [false] [false] [false] [false] [false] [false] [false] [false] The URL becomes /home. HTML, which still fires the popState event in home. HTML.

The difference between Chrome and Safari is related to popState event handling. As for how the browser is handled internally, xiaobian has not studied clearly. Although Chrome and Safari handle popState events differently, the URL rollback path is the same, complying with the last-in, first-out rule of historical records.

This is also true in real development: a URL change from /home/html to /cart.html is similar to a jump in single-page development. In cart.html, pushState is used to jump out of a single page and enter the login page, where the user clicks the browser to go back or the mobile gesture to return. This happens when Chrome and Safari render the page differently.

A popState event will be triggered on the corresponding window object. The popState event will be triggered on the corresponding window object. Again, pushState is the newly defined window object. Based on our tests above, it is possible to fire a PopState event. So don’t forget to do popState listening on both pages when you encounter the above situation.

3. The anchor point of a tag can also trigger a popState event method

Unlike pushState and replaceState, a popState event is immediately triggered by a change in the anchor point of the A tag. If the a tag changes the hash value, window.location changes the hash value and popState is triggered immediately. The answer is yes, and popState is triggered immediately.

As you can see in the Hash section, changing a hash value triggers a hashchange event, so changing a hash value triggers both a popState event and a hashchange event, but if the changed hash value is the same as the current hash value, The hashchange event is not raised, the popState event is raised. As we said earlier, window.location must set a hash value different from the current hash value to create a new history, whereas pushState does.

In addition, pushState, replaceState, and popState can also be used to implement hash routing if the browser supports pushState. Pushstate changes the hash value, and popState listens for changes in the hash value. If the browser supports the new feature of History, the router will use the new feature of History to jump to both hash and history modes.

Front-end routing application

History and hash are browser-owned features, and single-page routing takes advantage of these features. Without jumping out of the current Document, there is no difference between browsers except for the compatibility of History itself, while single-page development isto complete all interactions in a document. The perfect combination of the two has raised front-end development to a new level.

Vue-router and React-Router are the most popular routing state management tools. Although the implementation principle of the two is the same, but because of the different technology stack, the use of slightly different. When developing the React stack, most people prefer to use the React-router-DOM, which is based on the React-Router and adds some functions in the browser runtime environment.

Injection pattern

1. vue-router

Vue-router can be used globally in vUE projects thanks to vue.use(). With vue.use(), the VueRouter object is injected with a Vue instance, which is the root component. The root component passes the VueRouter instance down layer by layer, allowing each rendered child component to have routing capabilities.

import VueRouter from 'vue-router'
const routes = [
    { path: '/',name: 'home',component: Home,meta:{title:'首页'} }
]
const router = new myRouter({
    mode:'history',
    routes
})
Vue.use(VueRouter)
Copy the code

2. react-router-dom

The react-Router injection is implemented by placing a Router component at the top of the component tree, which is littered with Route components. The Router component at the top is responsible for analyzing and listening for URL changes, and the Route component below renders the corresponding components. In a complete single-page project, using the Router component to wrap the root component can accomplish a normal route jump.

import { BrowserRouter as Router, Route } from 'react-router-dom'; class App extends Component { render() { return ( <Router> <Route path='/' exact component={ Home }></Route> </Router> ) }}Copy the code

Based on the component

<outer-link/> <router-view/>

  • You can manipulate DOM to directly jump, and define which path to navigate to after clicking it. The corresponding component content is rendered to
    .

<BrowserRouter/> <HashRouter/> <Route/> <Link/> <Switch/>

  • The <BrowserRouter/> and <HashRouter/> components, as their names indicate, are used to distinguish routing modes and ensure that React projects have page-hopping capabilities.

  • The <Link /> component is similar to the <router-link/> component in vue-Router. It defines the target navigation path after clicking, and the corresponding component content is rendered by <Route />.

  • <Switch/> is used to convert the React-router from an inclusive route to an exclusive route. Once the match is successful, the react-router will not continue to be matched downward. Vue-router is an exclusive route.

Routing patterns

1. Vue-router can be classified into hash and history modes. In new VueRouter(), this is done by configuring the routing option mode.

  • Hash mode: The URL in the address bar contains #. Vue-router preferentially checks whether the browser supports pushState. If it supports pushState, it uses pushState to change the hash value for target route matching and render components. Popstate monitors browser operations to complete navigation. Use location.hash to set the hash value and hashchange to listen for URL changes to complete route navigation.

  • History mode: No # in address bar URL. The same idea is used to implement navigation in Hash mode. Unlike vue-router, which provides fallback configuration, the browser does not support history.pushState to control whether the route should revert to hash mode. The default value is true.

    Hashchange is a combination of location. Hash and hashchange, which is different from the implementation of Hash routing mode described above. This is also found in the recent reading of the uE-router source code. It will be fruitful!

2. The two common modes of react-router-dom are browserHistory and hashHistory, which can be implemented by wrapping the root component (usually <App>) directly with <BrowserRouter> or < hashHistory >.

  • The react-router implementation relies on history.js, which is a JavaScript library. <BrowserRouter> and <HashHistory> are based on the BrowserHistory and HashHistory classes of history.js, respectively.

  • The BrowserHistory class is implemented via pushState, replaceState, and PopState, but does not have vue-router-like compatibility. The HashHistory class is implemented directly through location.hash, location.replace, and hashchange, without precedence over the new history feature.

Nested routines and child routes

1. Vue-router nested routine

When new VueRouter() configates the routing table, Children are nested so that routing components, regardless of their level, are rendered to the location identified by the parent <router-view/>.

router.js

const router = new Router({
    mode:'history',
    routes: [{
        path: '/nest',
        name: 'nest',
        component: Nest,
        children:[{
            path:'first',
            name:'first',
            component:NestFirst
        }]
    }]
})
Copy the code

nest.vue

<div class="nest"> <router-view></router-view>Copy the code

first.vue

<div class="nest"> <router-view></router-view>Copy the code

The secondary route /first is set under /nest, and the components corresponding to the secondary route are rendered at the location identified by the component
that matches the primary route. When configuring child routes, path only needs to be the current path.

2. React-router Indicates the child route

The React-Router root component is rendered to the location specified by <Router/>, and the child route is used as a child component. The parent component specifies the object’s rendering location. If you want to achieve the above vue-router nesting effect, you need to set it like this:

route.js

const Route = () => (
    <HashRouter>
        <Switch>
            <Route path="/nest" component={Nest}/>
        </Switch>
    </HashRouter>
);
Copy the code

nest.js

Export default class Nest extends Component {render() {return (<div className=" Nest "> <Switch> <Route path="/nest/first" component={NestFirst}/> </Switch> </div> ) } }Copy the code

first.js

Export default class NestFirst extends Component {render() {return (<div className="nest"> <Switch> <Route exact  path="/nest/first/second" component={NestSecond}/> </Switch> </div> ) } }Copy the code

In the preceding command, /nest is the level-1 route, and/FITST is the component matching the level-2 route as a sub-component of the level-1 route. When the react-router defines the path of the child route, the path of the parent route must be complete.

Routing guard

1. Vue-router navigation guard is divided into three types: global guard, route exclusive guard and component internal guard. Mainly used to guard navigation by jumping or canceling.

A. Global guard

  • BeforeEach — global front hook (fires beforeEach route call, determines which route fires based on from and to)
  • BeforeResolve – Global resolution hook (similar to route. beforeEach, except that the resolution guard is called before navigation is confirmed and after all the guards and asynchronous routing components are resolved)
  • AfterEach – Global post-hook

B. Route exclusive guard

  • You can define the beforeEnter guard directly on the route configuration.

C. Component internal guard

  • BeforeRouteEnter – Called until the corresponding route to render the component has been confirmed. The component instance cannot be retrievedthisBecause the component instance has not been created before the guard executes.
  • BeforeRouteUpdate – called when the current route changes but the component is being reused
  • BeforeRouteLeave – called when navigating away from the corresponding route of the component

2. Prior to version 4.0, the onEnter and onLeave hooks were provided to implement functions similar to vue-Router navigation guard, but this method was cancelled after version 4.0.

Routing information

1. The vue – in the router
r o u t e r , The router,
The route objects

Vue-router During registration, each VUE instance is injected with router, Router, Router, and Route objects. Router is the router instance information, and the router is the router instance information by using push and replace methods. Router is the router instance information by using push and replace methods. Route hops are implemented using push and replace methods, and route provides the information about the currently active routes.

import router from './router'
export default new Vue({
    el: '#app',
    router,
    render: h => h(App),
})
Copy the code

2. The history and location objects in the react-router

History, location objects are provided in each component wrapped by
. The push and replace methods of this.props. History are used to implement route navigation. This.

const BasicRoute = () => (
    <div>
        <HeaderNav></HeaderNav>
        <HashRouter>
            <Switch>
                <Route exact path="/" component={Home}/>
            </Switch>
        </HashRouter>
    </div>
);
Copy the code

If you want to get history, location must be a component of the <Route /> package. So these two objects are not available in <HeaderNav/>, whereas the <Home/> component is.

Vue-router is a global configuration mode and React-router is a global component mode, but the functions presented to developers are virtually the same. Of course, the differences between vue-Router and React-Router are not limited. At the end of the day, the principle of front-end routing is the same no matter how it is implemented.

conclusion

The initial experience of front-end routing is coming to an end. Before deciding to study front-end routing in depth, Xiaobian was confident that it would not take much effort and time, but the fact is that there are more and more knowledge blind spots involved, and confidence is gradually collapsing. Fortunately, the ending is good and I have gained a lot. I also hope that this article “the core principle of SPA Routing trilogy” can make everyone gain something, even if it is only a knowledge point.

Xiaobian has been racing against time to prepare “SPA routing trilogy of MyVueRouter practice”, “SPA routing trilogy of VueRouter source code parsing” process, xiaobian believe that will not let you down, please look forward to it!

PS: Some of the article is personal opinion, if not, welcome to exchange, correction!