Needs and objectives:

When users fill in the form, they need to listen to the browser back button, and when the user clicks the browser back, they need to remind the user whether to leave. If not, you need to prevent browser rollback

How it works: Listen for popState events

The popState event is triggered when the browser’s active history entry changes.

Trigger condition: when the user clicks the browser back or forward button, when js calls history.back,history.go, history.forward

Note, however, that replaceState does not trigger a popState event when pushState is in js

window.addEventListener('popstate'.function(state) {console.log(state) // history.back() will trigger this line}) history.back()Copy the code

Currently, there is no API for multiple pages that directly blocks browser rollback, so we can hack it and use pushState and popState to do this.

When entering the page, manually pushState once, and the browser record entry will automatically generate a record. The length of history will be increased by 1. Then, listen for the popState event, and when it is triggered, a popup window is displayed for the user to confirm. Click Cancel, and then pushState again to restore the state before clicking. Click OK, and then manually call history.back to achieve the effect

frame

The implementation code

window.onload = (event) => {
	window.count = 0;
	window.addEventListener('popstate', (state) => {
		console.log('onpopState invoke');
		console.log(state);
		console.log(`location is ${location}`);
		var isConfirm = confirm('Are you sure you want to return? ');
		if (isConfirm) {
			console.log('I am going back');
			history.back();
		} else {
			console.log('push one');
			window.count++;
			const state = {
				foo: 'bar',
				count: window.count,
			};
			history.pushState(
				state,
				'test'// `index.html? count=${ // window.count // }&timeStamp=${new Date().getTime()}`); console.log(history.state); }}); console.log(`first location is${location}`);
	// setTimeout(function () {
	window.count++;
	const state = {
		foo: 'bar',
		count: window.count,
	};
	history.pushState(
		state,
		'test'// `index.html? count=${window.count}&timeStamp=${new Date().getTime()}`); console.log(`after push state locaiton is${location}`);
	// }, 0);
};
Copy the code

Implementation effect

Pit place

  1. Chrome does not take effect in some cases

In some cases, clicking back will not trigger the PopState event. The specific reason I understand is that Chrome itself has handled it. Because Of the abuse of PopState to control user backtracking, Google thinks it is bad for user experience, so it will directly ignore the middle page and return directly. For details, see English blogs

Previously, if you were on site A and clicked a link to go to nuisance site B, site B could automatically use pushState to add itself to your history and keep doing it, meaning you’d never get back to site A. Now, if the user didn’t click on something to request it, the browser will ignore the entry. As soon as the user clicks the back button, they can return to site A.

  1. In some cases Chrome does not work, but forms can work again

For the same reason 1, I guess chrome has done some monitoring on user behavior. If there is no user interaction on the page, Google thinks it can be returned directly. If there is user interaction, such as filling in the form content, popState event needs to be triggered

  1. If you are debugging locally, double-click demo.html file to open Chrome and the pushState occurs before the navigate event invalidates

Solution: None yet

Vue-router source code used in popState interpretation

Vue-router is mainly used for single pages, that is, changing the URL can render some components to render different pages without refreshing. Popstate also implements the History mode to listen for CHANGES in the URL, and then listens for browser returns in a similar way.

A URL -> B URL. When the user clicks “return”, the URL will revert to A URL first, and the popState callback will be triggered. Vuerouter determines that A URL needs to be changed to B URL according to the parameter passed by next callback. Pushstate (B URL) is required to prevent the browser from falling back

SRC /history/html5.js

First look at the use method:

BeforeRouteLeave (to, from, next) {// The hook function to call when the URL leavesif (
      this.saved ||
      window.confirm('Not saved, are you sure you want to navigate away? ')
    ) {
      next()
    } else {
      next(false// Call next(false) to prevent the browser from returning, see}}Copy the code
  setupListeners() {// for short, Dispatched const handleRoutingEvent = () => {const current = this.current // Avoiding first 'popstate' eventin some browsers but first
      // history route not updated since async guard at the same time.
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {
        return} this.transitionto (location, route => {// The custom transitionTo method is called to execute some queues, including various hook functionsif (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    }
    window.addEventListener('popstate', add popstate handleRoutingEvent) / / here to monitor function enclosing listeners. Push (() = > {window. RemoveEventListener ('popstate', handleRoutingEvent)
    })
  }
Copy the code

See SRC /history/base.js for the definition of transitionTo

transitionTo ( location: RawLocation, onComplete? : Function, onAbort? : Function ) { const route = this.router.match(location, This.current) this.confirmtransition (// call its own confirmTransition method route, // as a short, omit the source code)} confirmTransition(route: Route, onComplete: Function, onAbort? : Function) { const current = this.current const abort = err => { // changed after adding errors with // https://github.com/vuejs/vue-router/pull/3047 before that change, // redirect and aborted navigation would produce an err == nullif(! isRouterError(err) && 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 (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      returnabort(createNavigationDuplicatedError(current, route)) } const { updated, deactivated, activated } = resolveQueue( this.current.matched, route.matched ) const queue: Array<? NavigationGuard> = []. Concat (// define queue //in-component leave guards extractLeaveGuards(deactivated), // Execute beforeRouteLeave for current page // global before hooks this.router. BeforeHooks, // execute beforeRouteUpdate for new page //in-component update hooks
      extractUpdateHooks(updated),
      // in-config enter guards activated.map(m => m.beforeEnter), // async components resolveAsyncComponents(activated) ) this.pending = route const iterator = (hook: NavigationGuard, next) => {// The iterator will be executed once in the queue, see SRC /utils/asyncif(this.pending ! == route) {return abort(createNavigationCancelledError(current, route))
      }
      try {
        hook(route, current, (to: any) => {
          if (to === false) { // next(false// next()false) -> abort navigation, ensure current URL
            this.ensureURL(true) // See the definition of ensureURL belowtrueIs pushstate abort (createNavigationAbortedError (current, the route))}else if (isError(to)) {
            this.ensureURL(true)
            abort(to)
          } else if (
            typeof to === 'string' ||
            (typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {
            // next('/') or next({ path: '/' }) -> redirect
            abort(createNavigationRedirectedError(current, route))
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else{// Confirm transition and pass on the value next(to)}})} catch (e) {abort(e)}}Copy the code

For the definition of eusureURL, see SRC /history/html5.js

ensureURL (push? : boolean) {if(getLocation(this.base) ! == this.current.fullPath) { const current = cleanPath(this.base + this.current.fullPath) push ? PushState (current) : replaceState(current)Copy the code

More wonderful articles can be found on my blog. If there are any mistakes, please correct them