为什么Iframe中无法使用Vue-Router(下)

3,057 阅读5分钟

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.

-html.spec.whatwg.org/multipage/h…

简单来说不同的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 即可实现效果

20200607233903

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',
                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);
};

2.Ajax请求后可以后退

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

如果需要进行后退的话 就需要结合PushState了

当执行Ajax操作的时候,往浏览器history中塞入一个地址(使用pushState)(这是无刷新的,只改变URL);于是,返回的时候,通过URL或其他传参,我们就可以还原到Ajax之前的模样。

demo参考链接www.zhangxinxu.top/wordpress/2…

5.参考资料

HIstory APi 学习 :

developer.mozilla.org/en-US/docs/…

wangdoc.com/javascript/…

www.cnblogs.com/jehorn/p/81…

Vue-Router源码

liyucang-git.github.io/2019/08/15/…

zhuanlan.zhihu.com/p/27588422

Iframe相关问题学习:

github.com/WICG/webcom…

www.cnblogs.com/ranran/p/if…

www.coder.work/article/669…

www.yuanmacha.com/12211080140…

开发应用:

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, 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 == 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
    }
  }