为何在Iframe中不能使用Vue-Router

1.场景

在进行开发过程当中,直接使用了Vue-Router来进行页面跳转,可是出现了一些奇奇怪怪的bug,特花时间来进行相关调研并记录,若有不严谨或不正确的地方,欢迎指正探讨。javascript

问题

使用Vue-Router来进行页面跳转html

使用this.$router.push() 地址栏的连接不变,Iframe的src不变,可是Iframe的内容发生变化。vue

使用this.$router.go(-1) 来进行跳转,地址栏连接改变,Iframe的src改变,Iframe的内容也发生变化。html5

使用this.$router.href()能够进行跳转,且地址栏发生改变java

2.路由处理

说到路由跳转就不得不提Window.history 系列的Api了,常见的Vue-router等路由处理其本质也都是在经过该系列Api来进行页面切换操做。git

本次咱们讨论的就主要涉及 到Window.history.pushStateWindow.history.gogithub

Window.history(下文将直接简称为history)指向一个History对象,表示当前窗口的浏览历史,History对象保存了当前窗口访问过的全部页面网址。web

2.1History常见属性与方法

go() 接受一个整数为参数,移动到该整数指定的页面,好比history.go(1)至关于history.forward(),history.go(-1)至关于history.back(),history.go(0)至关于刷新当前页面ajax

back() 移动到上一个访问页面,等同于浏览器的后退键,常见的返回上一页就能够用back(),是从浏览器缓存中加载,而不是从新要求服务器发送新的网页vue-router

forward() 移动到下一个访问页面,等同于浏览器的前进键

pushState() pushState()须要三个参数:一个状态对象(state),一个标题(title)和一个URL。

*注意:pushState会改变url,可是并不会刷新页面,也就是说地址栏的url会被改变,可是页面仍保持当前。

总之,pushState()方法不会触发页面刷新,只是致使 History 对象发生变化,地址栏会有反应。

history.pushState({a:1},'page 2','2.html')

popState事件

每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。简单能够理解为,每次咱们须要修改url 那么一定是先出发了popState事件,浏览器的地址栏随后才会发生改变。

注意,仅仅调用pushState()方法或replaceState()方法 ,并不会触发该事件,**只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用History.back()、History.forward()、History.go()方法时才会触发。**另外,该事件只针对同一个文档,若是浏览历史的切换,致使加载不一样的文档,该事件也不会触发。

2.2Vue-Router的实现

modemode

  
  #push src/history/html5.js 

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

function pushState (url, replace{
    saveScrollPosition();
    // try...catch the pushState call to get around Safari
    // DOM Exception 18 where it limits to 100 pushState calls
    var history = window.history;
    try {
      if (replace) {
        // preserve existing history state as it could be overriden by the user
        var stateCopy = extend({}, history.state);
        stateCopy.key = getStateKey();
        history.replaceState(stateCopy, '', url);
      } else {
        history.pushState({ key: setStateKey(genStateKey()) }, '', url);
      }
    } catch (e) {
      window.location[replace ? 'replace' : 'assign'](url);
    }
  }

#go src/history/html5.js 
  go (n: number) {
    window.history.go(n)
  }

以上是Vue-router再history模式下push和go的源码,可见其主要的实现是经过History Api来实现跳转的。

2.3Vue-Router是如何实现单页应用的呢?

vue-router 主要用来作单页面,即更改 url 无需刷新可以渲染部分组件达到渲染不一样页面的效果,其中 history 模式监听 url 的变化的也是由 popstate 实现的,而后监听浏览器返回的方法也是大同小异。

原理是,A url-> B url,此时用户点击返回时,url 先回退到 A url,此时触发 popstate 回调,vuerouter 根据 next 回调传参是 false 判断须要修成 A url 成 B url,此时须要将进行 pushstate(B url),则此时就实现了阻止浏览器回退的效果

Ps:篇幅缘由,源码在文章底部附上。

那么在进行了Iframe嵌套后会有什么不同呢?

3.IFrame嵌套状况下问题解决

The sequence of Documents in a browsing context is its session history. Each browsing context, including child browsing contexts, has a distinct session history. A browsing context's session history consists of a flat list of session history entries.

Each Document object in a browsing context's session history is associated with a unique History object which must all model the same underlying session history.

The history getter steps are to return this's associated Document's History instance.

-https://html.spec.whatwg.org/multipage/history.html#joint-session-history

简单来讲不一样的documents在建立的时候都有本身的history ,同时内部的document在进行初始化时候具备相同的基础HIstory。

如上,当咱们从页面A进行跳转之后,Top层,和内嵌Iframe层初始时是具备相同的history,所以,当咱们进入页面后,不管是在页面B 仍是页面C中使用window.history.go(-1)都可以实现相同的效果,即返回页面A,且浏览器的URl栏也会随之发生改变。

当咱们从hybrid页面跳向hybrid的时候

以下,此时若是在新的页面内使用go(-1),则可能会出现问题【当页面A和页面B的History不一致时】,可是除了咱们手动去pushState改变,大部分状况页面A和页面B的history是彻底一致的所以也就不会出现History不一致的问题了。

那么来看一下咱们一开始遇到的问题:

注意:如下仅仅针对Chrome浏览器,不一样浏览器对于Iframe中的HIstory Api处理方式可能会存在不同。

1.使用this.$router.push() 地址栏的连接不变,Iframe的src不变,可是Iframe的内容发生变化。

2.使用this.$router.go(-1) 来进行跳转,地址栏连接改变,Iframe的src改变,Iframe的内容也发生变化。

3.使用this.$router.href()能够进行跳转,且地址栏发生改变

1.直接调用Router.push 至关于咱们在Iframe中调用了pushState,可是因为pushState是不会主动触发popstate的,因此外层的popstate是没有被触发,所以外层的url并没有改变,可是内层因为VueRouter经过对pushState的callBack事件来进行的后续操做,所以能够实现对popState事件的触发,从而实现了在将新的url push到history中之后,并进行了页面的跳转。

2.使用this.$router(-1) 能够实现跳转的缘由在于,在咱们进入一个hybrid页面的时候,iframe的history会被初始化和window彻底相同,也就是说,这个时候咱们在Iframe中执行window.go(-1)取到的url 是和直接在Top执行Window。因此这个时候执行Router.go(-1)是能够正常运行且返回上一个页面的。

3.本质仍是对remote方法进行封装 。

关于页面IFrame中history Api的应用仍是存在着一些争议和问题,在W3C的TPAC会议上也都有在进行相关的讨论

虽然最后有了一些共识,可是对于各个浏览器来讲,兼容性仍是不太一致。所以,建议你们在Iframe中使用history系列api时,务必当心并增强测试。

从上来看,是很是不科学的,iframe中能够影响到Window的history,Chorme也认可这是一个漏洞

4.实际开发中的应用

1.返回检测

1.实际开发需求:

用户填写表单时,须要监听浏览器返回按钮,当用户点击浏览器返回时须要提醒用户是否离开。若是不须要,则须要阻止浏览器回退

2.实现原理:监听 popstate 事件

popstate,MDN 的解释是:当浏览器的活动历史记录条目更改时,将触发 popstate 事件。

触发条件:当用户点击浏览器回退或者前进按钮时、当 js 调用 history.back,history.go, history.forward 时

但要特别注意:当 js 中 pushState, replaceState 并不会触发 popstate 事件

window.addEventListener('popstate'function(state{
    console.log(state) // history.back()调用后会触发这一行
})
history.back()

原理是进入页面时,手动 pushState 一次,此时浏览器记录条目会自动生成一个记录,history 的 length 加 1。接着,监听 popstate 事件,被触发时,出弹窗给用户确认,点取消,则须要再次 pushState 一次以恢复成没有点击前的状态,点肯定,则能够手动调用 history.back 便可实现效果

2020060723390320200607233903

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('确认要返回吗?');
        if (isConfirm) {
            console.log('I am going back');
            history.back();
        } else {
            console.log('push one');
            window.count++;
            const state = {
                foo'bar',
                countwindow.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',
        countwindow.count,
    };
    history.pushState(
        state,
        'test'
        // `index.html?count=${window.count}&timeStamp=${new Date().getTime()}`
    );
    console.log(`after push state locaiton is ${location}`);
    // }, 0);
};

2.Ajax请求后能够后退

在Ajax请求虽然不会形成页面的刷新,可是是没有后退功能的,即点击左上角是没法进行后退的

若是须要进行后退的话 就须要结合PushState了

当执行Ajax操做的时候,往浏览器history中塞入一个地址(使用pushState)(这是无刷新的,只改变URL);因而,返回的时候,经过URL或其余传参,咱们就能够还原到Ajax以前的模样。

demo参考连接https://www.zhangxinxu.top/wordpress/2013/06/html5-history-api-pushstate-replacestate-ajax/

5.参考资料

HIstory APi 学习 :

https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event

https://wangdoc.com/javascript/bom/history.html

https://www.cnblogs.com/jehorn/p/8119062.html

Vue-Router源码

https://liyucang-git.github.io/2019/08/15/vue-router%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

https://zhuanlan.zhihu.com/p/27588422

Iframe相关问题学习:

https://github.com/WICG/webcomponents/issues/184

https://www.cnblogs.com/ranran/p/iframe_history.html

https://www.coder.work/article/6694188

http://www.yuanmacha.com/12211080140.html

开发应用:

https://www.codenong.com/cs106610163/

Vue-Router实现源码:

#src/history/html5.js

beforeRouteLeave (to, from, next) { // url离开时调用的钩子函数
    if (
      this.saved ||
      window.confirm('Not saved, are you sure you want to navigate away?')
    ) {
      next()
    } else {
      next(false// 调用next(false) 就实现了阻止浏览器返回,请看下面
    }
  }
setupListeners () {
        // 为简略,省略部分源码
    const handleRoutingEvent = () => {
      const current = this.current

      // Avoiding first `popstate` event dispatched in 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 => { // 这里调用自定义的transitionTo方法,其实就是去执行一些队列,包括各类钩子函数
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    }
    window.addEventListener('popstate', handleRoutingEvent) // 在这里添加popstate监听函数
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }
#下面看 transitionTo 的定义,参见 src/history/base.js
  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    const route = this.router.match(location, this.current)
    this.confirmTransition( // 调用自身的confirmTransition方法
      route,
      // 为简略,省略部分源码
    )
  }

  confirmTransition (route: Route, onCompleteFunction, 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 == null
      if (!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()
      return abort(createNavigationDuplicatedError(current, route))
    }

    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )

    const queue: Array<?NavigationGuard> = [].concat( // 定义队列
      // in-component leave guards
      extractLeaveGuards(deactivated), // 先执行当前页面的beforeRouteLeave
      // global before hooks
      this.router.beforeHooks, // 执行新页面的beforeRouteUpdate
      // 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) => { // iterator将会在queue队列中一次被执行,参见src/utils/async
      if (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// 关键看这里:请看下面ensureURL的定义,传true则是pushstate
            abort(createNavigationAbortedError(current, 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)
      }
    }
        // 为简略,省略部分源码
  }

#eusureURL 的定义,参见 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) // 执行一次pushstate    }  }
相关文章
相关标签/搜索