爲何在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    }  }
相關文章
相關標籤/搜索