为什么在Iframe中不能使用Vue-Router

1.场景

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

问题

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

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

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

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

2.路由处理

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

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

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

2.1History常见属性与方法

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

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

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
    }
  }
posted @ 2021-01-21 15:54  NAME_IS_NaN  阅读(4115)  评论(3编辑  收藏  举报