The foreword 0.

As for my first contact with changing URLS without refreshing, I came across it during a job interview with the JAVA architect of my current company. To be honest, I had never really experienced this kind of need before, so I was overwhelmed by it (/ laughing and crying). Recently, I suddenly remembered this, so I studied it with great interest. I feel that there are some things that can be shared. This article is a long one, which is roughly divided into the following parts

  1. None Refresh Application scenarios of changing URLS
  2. The explanation of the History API is really an explanation of the unclear parts of the MDN documentation
  3. A demo of changing URLS without refreshing in a multi-page application
  4. No demo of refreshing changing URLS in a single page
  5. Use of the History API in vue-router

1. Application scenarios of CHANGING URLS are not refreshed

As far as I am concerned, the application scenario of this method is mainly to use URL to store the information we need: one is that we need a temporary data storage area, such as SPA paging/query function, we will splice the query parameters into URL and store them temporarily; The other is that when we do the sharing content, users can copy the URL and save the user’s operation content at the same time. A typical example is the commodity page of Taobao. A lot of the SKU content that we choose is posted to the URL and shared with our friends so they don’t have to do anything about it.

2.Window.History API

The properties and methods of the History object are clearly visible from the console,

Properties/methods function
length Indicates the browsing history length of the current page. The length of the page just opened =1
scrollRestoration Set the scroll recovery behavior
state When the browser URL has parameters, you can check them in state
back Page rollback (no error is reported when the page is rolled back to the last history)
forward Page forward (forward to the first, no effect, no error)
go(number) You can jump to the specified browsing history with a positive number and jump forward, or backward if the number is positive
pushState Write a history to the browser, but the browser will not load the address
replaceState Replace a history record
popState An event that is triggered when browsing history changes
  1. ScrollRestoration Sets the rolling recovery behavior

In layman’s terms, when we jump from the current page to page A and then back from page A, should our page automatically scroll back to where we were before? ScrollRestoration the default value is auto, and the default is rolling restoration. The other value is manual, which means it will not be restored by default, and we can choose to scroll the page manually. The following image should make it more intuitiveBefore we talk about these two methods, let’s talk about the History state property. The state property represents the current state value and can be read from it. We can think of state as a stack, and as the page history increases, the browser pushes a state to the stack, and our state points to the state at the top. When we fall back, state is pushed off the stack, and the previous state becomes the top of the stack. This makes pushState easier to understand by pushing a new history into state. The syntax for pushState is simple:

history.pushState(state, title, url); @state: An object associated with history, whose role will be reflected in popState later. One thing to note is that this object must be a serializable object. If we pass a DOM object into it, we'll get an error. @ url: the url value of the new history will be displayed in the address bar, but it will not load the resource of this URL. This is also the key point for us to change the URL without refreshing, but it must be noted that the old and new urls must be the same as each other, otherwise an error will be reported.Copy the code

On a side note, pushState has the same effect as window.localtion in one case, where we expect to change the url hash. As we all know, browsers don’t load content after #, so in this case, location and history.pushState are identical. 3. PopState is triggered when the browser history changes. When this event is triggered, the browser makes a copy of the State object, let’s call it copyState. If it is a new history, a new state is pushed at the top of copyState, if it is a back history, the state at the top of copyState is removed. Although I have not used this method in actual scenes, I still feel there is a lot of room for operation, such as the following small routines:

Note that popState triggers only if the two histories must be the same as each other. If they are not, the event will not respond. Second, the history must be created with pushState or replaceState

3. Implementation of multi-page applications

As long as you read the above, I’m sure you already have a rough idea of how to implement it. Control the browser’s history by using the pushState/replaceState apis. Select pushState if you want to record history, or replaceState if you want to replace the record. Note that improper use of pushState can cause the browser’s back button to revert back to some messy history.

Specific code is not posted, a simple horse, it is estimated that you will also knock two out of the keyboard. If the URL in the address bar is the path to your local file, it will send you a bright red error warning. (HBuilder’s built-in server is ok if online server is not available)

Some people may not be very clear about the specific application scenarios up to now.

In this way, we can assume an application scenario: this weekend, you are going to haidilao dinner with your friends. You choose the one you think is suitable from the page of choosing a restaurant, and then share the url with your friends for reference. Alas, the link you share can’t be the same as the one you just entered, because then friends would still have to repeat the same process when clicking on this site, so the link we share should contain some key information about our selection. Take a look at the simulation below:

Let newState = {timeStamp: new Date().gettime (), // Share n link invalid after a long time address: AreaCode :'011011', shopName:' Haidilao XX Shop ', Function getQueryString(obj) {let newQuery = [] if (obj instanceof Object! = true) return '' if (Object.keys(obj).length == 0) return '' for (let x in obj) { newQuery.push(`${x}=${obj[x]}`) } return `? ${newQuery.join('&')} '} Function changeUrl(obj) {let newUrl = window.location. pathName + getQueryString(obj) function changeUrl(obj) {let newUrl = window.location. pathName + getQueryString(obj) window.history.replaceState(newState, null, 'a.html'); }Copy the code

Ok, in the above code, we implemented a crude example of adding parameters to the URL without refreshing. Through some actions of the user in the page, such as clicking the query button, or triggering the change event in the drop-down box, to trigger our changeUrl, to realize the saving of some user operations.

4. The implementation of the single-page application was originally intended to write a method to implement routing in SPA (taking Vue as an example), but later I thought it was inappropriate and felt that it would be better to write the specific implementation of routing. Maybe the implementation principle in the plug-in is what you want to see, and that’s kind of teaching people how to fish.

So I’ll code the methods here, and then look at how vue-Router in Vue makes use of the History API. Here is a demo picture for you to feel:

Description:

The above part of the content, mainly in the USE of url search field, to achieve the purpose of saving operation records. Why adopt this model?

  1. If the search criteria are not recorded in a similar list page, we click on one of the ids to view the details, and the route will switch to the details page accordingly, which is OK. But when we finish browsing and return from the details page, the list page reloads and we have to re-enter our search criteria. The experience is pretty awful, especially when there are so many options.

  2. The keep-Alive component caching feature in Vue can help solve this problem. I was smart enough to realize that using Keep-Alive did guarantee that components would not be re-rendered, but some of the changes made on other pages didn’t show up on our list page in time. Because the data on the page is still old data from the last request, this can be misleading to the user.

  3. Like the Taobao page mentioned earlier in this article, when users share our URL links, the sharer does not need any additional action.

###### implementation: first to a cool ugly cool ugly flow chart###### PS: No silver bullet! The following code is for reference only and should be modified according to actual scenarios. ######1. User operations generate parameters: key-value In the preceding example, each valid operation is saved in the form of key=value. UserRoles =userRoles, search content =content, currentPage =currentPage

2.Handler

Define a Handler that handles the conversion of key-value to URL parameters. Sample code:

Import router from '@/router' import store from '@/store' let temporary = {} export function CreateTemporary (){temporary = getRouteQuery()} /** * setTemporary URL cache data temporary * @param key extended attribute name * @param value Extended attribute value */ export function setTemporary(key, Value) {temporary[key] = encodeURI(value)} /** * getRouteQuery Query object * key-point: must be a deep copy */ function GetRouteQuery () {return JSON parse (JSON. Stringify (router) currentRoute) query))} / * * * extendQuery based on the original query object / export function extendQuery() {let query = getRouteQuery() for (let key in) temporary) { query[key] = temporary[key] } router.replace({ query: } /** * directExtendQuery immediately writes to query, Export function directExtendQuery(key, value) { let query = getRouteQuery() query[key] = value router.replace({ query: query }) }Copy the code

We specify that the operation that changes the URL is called a write operation.

  1. Define a registertemporary, used for staging user operations to write data from the staging to the URL when a write operation is triggered.
  2. Obtaining parameters are part of the route objects, in the router. CurrentRoute. Available in the query. The only catch is that you must make a deep copy of the routing object, otherwise you will be operating directly on the original query object. The contents of the register are converted to a Query object. In the browser, Chinese characters added to the URL will be automatically decoded, so we need to take an extra step in decoding Chinese characters.
  3. To change the route, call vue-router’s replace method (there is no need to record the history, so push method is not used). In the actual operation, it is found that there are two requirements for changing the route:

1. Perform an operation => Wait for the trigger. If the user selects a condition on the page, after the selection is complete, the user will wait to click the query button to trigger the selection condition. So, when we select a condition, we put the data into the store first, and when we click the query button, we take the data out of the cache. 2. Perform an operation => Trigger immediately. To change the currentPage number, change currentPage in the url immediately

3. Parse the URL

Sample code:

/** * @function getUrlQuery * @description * @argument specifies the argument norClass. * @argument specifies the argument decodeURI. */ export function getUrlQuery(norClass, speClass, norDefault = '', speDefault = '') { return Object.assign({}, loopFetch(norClass, DirectQuery, norDefault), loopFetch(speClass, decodeQuery, speDefault)} * @param {Object} Object * @param {Function} fn * @param {string} default */ Function loopFetch(object, fn, defaults) { if (! object) { return {} } let o = {} for (let i in object) { o[i] = fn(object[i], } return o} /** * directQuery/decodeQuery export function directQuery(key, decodeQuery) defaults = '') { return router.currentRoute.query[key] ? router.currentRoute.query[key] : defaults } export function decodeQuery(key, defaults = '') { return router.currentRoute.query[key] ? decodeURI(decodeURI(router.currentRoute.query[key])) : defaults }Copy the code

In the above flowchart, the purpose of url parsing is two, so we need to expose two interfaces to meet the requirements.

  1. Controls the page displaydirectQuery/decodeQueryFunction. The reason why two functions are defined is to deal with the situation that value contains Chinese characters. Encode is carried out twice during transcoding, so both sides of decode are also needed during decoding.
  2. indirectQuery/decodeQueryFunction based on the extensiongetUrlQueryFunction to get the parameters to send the request.

After using urls to store data, the request parameters need to be retrieved accordingly: fields that refer to using urls should be retrieved from urls instead. GetUrlQuery function for different fields, respectively using directQuery/decodeQuery function, return a parameter dictionary object.

4. The echo
import * as routeFn from '@/util/route-query' //... Methods :{... _initQuerys() {this.pagination. Current = + routefn.directQuery ('currentPage', 1) this.content= routeFn.decodeQuery('content') this.userRole = routeFn.decodeQuery('userRole') }, ... Routefn.createtemporary () this._initquerys ()} create () {routefn.createTemporary () this._initquerys ()}Copy the code

The code is obvious and won’t be repeated.

5. Request

As mentioned earlier, request parameters that refer to a URL are fetched from the URL instead. So, the way we request it needs to be adjusted.

import * as routeFn from '@/util/route-query' ... GetParam () {// Get the optional object let param = {mobileName: 'content', userRole: 'userRole'} return routefn. getUrlQuery({}, param,this.$route.query)}, // The request method is slightly different from the normal request because of the constraints on the list page pass. SendRes () {let params = {apiPath: userList, // API address currentPage: routeFn.directQuery('currentPage', 1,this.$route.query), pageSize: this.pageSize, optional: Object.assign({}, this.getParam(), { roleList: isNeed(this.roleOptions) }) } sendInfo(params).then((res) => { if (res.data.code == 200) { this.pagination.total = res.data.userCount; this.tableData = res.data.userListInfoList || []; }})}Copy the code

Current interface restrictions, API, currentPage, pageSize are mandatory, and other content is not mandatory. Therefore, an optional field is used to control the selected content. When dealing with parameters, use a combination of multiple objects: the default empty object (as the default), the parameter object obtained through the URL, and the object composed of other parameters, which can cover all parameters. ###5. The number of Vue users in the country where the History API is applied is quite similar, so let’s take vue-router as an example. It is believed that most Vue users have read the vue-router documentation, and the use of vue-Router should be 66, so, the introduction of vue-Router is omitted here (omitted 200 words….) -_ -). When I look at vue-Router source code, I feel most closely related to History, should be vue-Router provides programmatic navigation method and internal simulation browser scroll processing, so here is a simple analysis of these two.

1. Programmatic navigation (history.pushState and history.replacestate)

Let’s pick out some of the text from the document:

router.push(location, onComplete? , onAbort?) To navigate to different urls, use the router.push method. This method adds a new record to the history stack, so when the user clicks the browser back button, it returns to the previous URL. This method is called internally when you click, so clicking is equivalent to calling router.push(…). .

router.replace(location, onComplete? , onAbort?)

Much like router.push, the only difference is that it does not add a new record to history, but replaces the current history record with its method name.

router.go(n)

This method takes an integer that means how many steps forward or backward in the history, similar to window.history.go(n).

How about, how familiar are push/replace/go methods with? Yes, these methods is not Histroy pushState replaceState/go method is short… Vue-router checks browser compatibility first

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 && 'pushState' in window.history })()Copy the code

If android 2.x or Android 4.0 Windows Phone (not Safari or Chrome) returns false to indicate that pushState is not supported, preventing an error when calling window.history.xx. Here is the code to call the History API

export function pushState (url? : string, replace? : Boolean) {saveScrollPosition() // Save the current page scroll position const history = window.history try {if (replace) {history.replacestate ({saveScrollPosition() // Save the current page scroll position const history = window.history try {if (replace) {history.replacEstate ({ key: _key }, '', url) } else { _key = genKey() history.pushState({ key: _key }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url) } } export function replaceState (url? : string) { pushState(url, true) }Copy the code

Exposed two methods with the same pushState/replaceState, based in histroy. PushState/replaceState method on a layer of packaging to take instead of the native method. Safari limits the number of calls to pushState to 100, which will result in a DOM exception no. 18. So try catch is used to bypass the call to the pushState API in Safari. If an error is reported, the assign/replace method of Location is used instead. Instead of passing in three parameters, you just need to pass in one destination address. Here we put all the processing in pushState, using a Boolean replace field to determine whether to push or replace. There is a getKey and saveScrollPosition method, which is used to record the current position of the browser, as discussed below. If you are interested in the above exception, you can run this code in Safari to check the result:

let i = 0; setInterval(()=>{history.pushState("",null,Date.now()); console.log(i++)},200)Copy the code

The router.push/replace implementation is essentially a shell of pushState/replaceState. The effect of this shell isto add these two methods to the History class created by vue-Router and expose the user.

The other router methods :go/back/forward use history.go

go (n: number) {
  this.history.go(n)
}
back () {
  this.go(-1)
}
forward () {
  this.go(1)
 }
Copy the code
2. The rolling behavior (history. History and popstate. ScrollRestoration)

The scrolling behavior uses front-end routing, and when switching to a new route, you want the page to roll to the top, or keep the original scrolling position, as when reloading the page. Vue-router can do this, and better, by letting you customize how the page scrolls when switching routes. ###### Note: This feature is only available in browsers that support history.pushState.

Most people probably haven’t used the scrolling behavior provided by vue-Router, so I’ll explain it here

First, the purpose of scrolling behavior is to allow you to control whether or how the page scrolls when switching routes.

Vue-router provides a scrollBehavior method that works like scrollTo(x,y)

const router = new VueRouter({ routes: [...] , scrollBehavior (to, from, savedPosition) {return (x: 0, y: 0); Return {selector: string, offset? If (savedPosition) {return savedPosition} else {return {x: 0, y: 0}}}})Copy the code

The example code above demonstrates three operations. The first means that the page always scrolls to the top when switching routes. The second means that you can scroll to an element when switching routes. Because Vue uses document.querySelector to get the element, the string here passes in the element selector name. The offset parameter is not mandatory. If it exists, it represents the distance to be added to the element. The third way is to simulate the behavior of the browser in a multi-page application, where the page always scrolls to the top when a new page is opened (with histroy recording added). When retracting, the system automatically retreats to the last browsing position (Save Position is the location information of the last browsing record, equal to null when a new page is opened).

The next step is to analyze the implementation of vue-Router.

How does scrollBehavior work?

When the browser adds a push/replace or switches a history, vue-Router determines whether the user is using the scrollBehavior correctly. If the user does not use this method, no action is taken. That’s why when we open a new route, the scroll bar stays in its current position. After determining that the user has correctly used the scrollBehavior, it continues to determine whether popState is triggered. If so, it retrieves the current historical position from the browser’s history and assigns it to savePosition; otherwise, it sets savePosition to null. Finally, the scrollBehavior set by the user is judged. If the user passes in a specific coordinate, window.scrollto () is called directly. If the user wants to scrollTo an element, the calculation rules for the element position are called. To get the distance to scroll and call window.scrollto ().

What does ###### do with scrolling positions in page browser history? The router defines a Dictionary to record the user’s location. When we trigger a route change, the router writes a location record to the dictionary. The dictionary key is the time that triggered the route change, so that the time can be used to ensure that the key is unique, and the value is an object that records the coordinates of that position. So, dictionary objects run like this:

Dictionary :{'1516964694736':{x: 0, y: 0}, '1516964698142':{x: 0, y: 0} 100}, '1516964722214':{x: 130, y: 361},}Copy the code
How do I trigger write behavior?

For new/replace history, vue-Router defines pushState and replaceState to write a piece of data, key= ‘current time’, value= current location information, to the object where the location is saved.History. pushState({key: _key}, ", url) // Write the time to state for lookup and updateAs mentioned earlier in Histroy, the popState event is triggered when the browser accesses an existing history page. Therefore, for the switch history, vue-Router instantiates a listener for a POPState event. If the POPState event is triggered, the location information in the browser history is updated using the key in state.

6. Conclusion

Unconsciously code nearly 5000 words, still feel a large part of want to say, but piecemeal how to mouth. I’ll reorganize it later and try to add more to the article. But the writing is not good, if you want to learn, but do not understand the place, welcome your message. (๑ ¯ ◡ ¯ ๑)